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);
}
}
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 类未在配置中列出、列在错误的部分(
mixinsvsclient),或者位于与配置的package不同的包中。 - 客户端目标位于通用配置中:在专用服务器启动时崩溃。
- 从普通代码直接引用 Mixin 类(
new LivingEntityMixin()),或将非 Mixin 辅助类放在 Mixin 包中。 - 非
@Unique、未加前缀的字段与另一个模组的 Mixin 冲突。 - 关闭
defaultRequire以“修复”失败的 Mixin——这隐藏了 bug,并发布了一个静默不执行任何操作的模组。 - 类调整器仅在
build.gradle/fabric.mod.json之一中声明,或未经验证。