Community程式設計與開發github.com

vvs1off/fabric-mixins

在 Fabric 模組中編寫、審查或修復 Mixin,並在 Fabric API 事件與 Mixin 之間做出選擇,包含 @Inject、@Redirect、@ModifyVariable、更安全的 MixinExtras 注入器(@WrapOperation、@ModifyExpressionValue、@WrapWithCondition、@Local)、存取器/調用器、類別修改器(存取擴展器)、跨模組相容性以及使用 mixin.debug.export 進行除錯。每當需要改變原版行為、讀取私有欄位,或 Mixin 無法套用時使用。

fabric-mixins 是什麼?

這個 AI 技能提供了一份詳盡的 Fabric Mixin 開發指南,幫助開發者在 Minecraft 模組開發中正確且安全地使用 Mixin。它強調 Mixin 的脆弱性,並建議優先考慮 Fabric API 事件、資料驅動的擴展點、存取器/調用器和類別修改器,最後才使用注入器。技能詳細解釋了 MixinExtras 注入器(如 @WrapOperation、@ModifyExpressionValue)相較於傳統 @Redirect 和 @ModifyVariable 的優勢,因為前者支援多個模組的鏈式堆疊,相容性更高。此外,還涵蓋了正確的目標設定方法(使用反編譯後的原始碼)、Mixin 配置慣例、跨模組相容性檢查清單(避免在常用方法頭部取消、優先使用可鏈式注入器)、以及用於除錯的 JVM 參數設定。對於需要修改原版行為但又擔心與其他模組衝突的開發者來說,這份指南至關重要。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/vvs1off/fabric-skills/tree/main/skills/fabric-mixins

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Fabric Mixin

Mixin 在載入時修補 Minecraft 的字節碼。它們功能強大但也很脆弱:每個 mixin 都承諾某個特定形狀的方法在下一個版本中仍然存在,並且沒有其他模組會不相容地修改它。盡可能少寫,並盡可能地精確。

決策順序 - 在編寫 mixin 之前先嘗試這些

  1. Fabric API 事件或註冊表。 優先搜尋 Fabric API:生命週期事件 (ServerLifecycleEvents、ClientTickEvents)、玩家事件 (UseBlockCallback、AttackEntityCallback、PlayerBlockBreakEvents)、HUD (HudElementRegistry)、螢幕 (ScreenEvents)、戰利品 (LootTableEvents)、指令 (CommandRegistrationCallback)、網路、物品提示、資源重新載入監聽器等。瀏覽 https://github.com/FabricMC/fabric-api (每個模組的 api 套件) 和 Fabric 文件。事件在不同版本間穩定,並可免費與其他模組組合。
  2. 原版擴展點。 資料驅動的內容 (標籤、戰利品表、合成配方、資料元件、註冊表) 通常不需要任何程式碼修補。
  3. 存取器 / 調用器 Mixin - 當你只需要讀取/寫入私有欄位或呼叫私有方法時。
  4. 類別修改器 (存取擴展器) - 當你需要子類化一個 final 類別、覆寫一個 final 方法,或引用一個非公開的類別時。
  5. 注入器 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類別未列在設定檔中,或列在錯誤的區段 (mixins vs client),或位於與設定檔的 package 不同的套件中。
  • 將僅限用戶端的目標放在通用設定檔中:這會導致專用伺服器在啟動時崩潰。
  • 從一般程式碼中直接引用 mixin 類別 (new LivingEntityMixin()),或將非 mixin 的輔助類別放在 mixin 套件中。
  • 非 @Unique、未加前綴的欄位與其他模組的 mixin 衝突。
  • 為了 "修復" 失敗的 mixin 而關閉 defaultRequire - 這會隱藏錯誤並發布一個默默地什麼都不做的模組。
  • 僅在 build.gradle / fabric.mod.json 其中之一宣告了類別修改器,或是沒有進行驗證。

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相關技能