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 是什么?

该技能为 Fabric 模组开发者提供了关于 Mixin 的全面指南,强调了 Mixin 的强大与脆弱性——每次使用都承诺了特定方法和形状在下一版本中仍然存在,且没有其他模组不兼容地修改它。技能首先推荐使用 Fabric API 事件、原版扩展点、访问器/调用器和类调整器作为 Mixin 注入器的替代方案,只有在这些方法都不可行时才考虑注入器。它详细介绍了 MixinExtras 库(从 Fabric Loader 0.15+ 开始内置)提供的安全注入器,如 @ModifyExpressionValue、@WrapOperation 和 @WrapWithCondition,这些注入器支持多个模组链式操作,而 @Redirect 和 @Overwrite 则会导致冲突。技能涵盖了访问器/调用器、类调整器(访问扩充器)的配置、跨模组兼容性检查清单以及调试技巧(如 export 和 verbose 参数)。常见错误包括重复发明 Fabric API 已有的功能、猜测方法名或描述符、在公共模组中使用 @Overwrite、以及忘记在 @WrapOperation 中调用 original.call()。该技能适用于 Claude Code、Cursor、Codex 等 AI 助手,帮助开发者在编写 Mixin 时做出正确决策。

兼容平台~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);
    }
}
  • CallbackInfo 用于 void 目标,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 冲突。
  • 关闭 defaultRequire 以“修复”失败的 Mixin——这隐藏了 bug,并发布了一个静默不执行任何操作的模组。
  • 类调整器仅在 build.gradle / fabric.mod.json 之一中声明,或未经验证。

Individual skills in this repo

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

相关技能