Skip to content

Mixin 入门教程

This content is not available in your language yet.

本教程假定你完全不了解 Mixin,但已经会写 Mindustry Java 模组。我们会用 Mindustry 里的真实例子,一步步建立对 Mixin 的完整认识。

让游戏在完成启动时打印一行我们自己的日志——不修改游戏代码,只靠注入。

  1. 声明 Mixin 目标。 确保项目已配置好 Mixin 注解处理器(见快速开始),并在 copper.mod.json 中声明了 Mixin 目标:

    {
    "version": 1,
    "meta": {
    "id": "example:examplemod",
    "main": "example.examplemod.ExampleMod",
    "mixins": {
    "mindustry": "mixins/mindustry.json"
    }
    }
    }
  2. 创建 Mixin 配置。 在 res/assets/copper/mixins/mindustry.json 创建 Mixin 配置:

    {
    "version": 1,
    "config": {
    "required": true,
    "minVersion": "0.8",
    "package": "example.examplemod.patch.impl",
    "mixins": {
    "*": ["MyPatch"]
    }
    }
    }
  3. 编写 Mixin 类。 在 src/你的包名/patch/impl/MyPatch.java 创建 Mixin 类:

    package example.examplemod.patch.impl;
    import arc.util.Log;
    import mindustry.Vars;
    import org.spongepowered.asm.mixin.Mixin;
    import org.spongepowered.asm.mixin.injection.At;
    import org.spongepowered.asm.mixin.injection.Inject;
    import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
    @Mixin(Vars.class)
    public abstract class MyPatch {
    @Inject(method = "finishLaunch", at = @At("RETURN"))
    private static void addInjectedLog(CallbackInfo ci) {
    Log.info("Injected to Vars::finishLaunch by example mod.");
    }
    }
代码作用
@Mixin(Vars.class)声明本类是一个 Mixin,目标类是 mindustry.Vars
@Inject(...)声明注入点:把下面的方法注入到目标方法里
method = "finishLaunch"要注入的目标方法名
at = @At("RETURN")注入位置:目标方法的所有 return 语句之前
CallbackInfo ci注入回调的固定参数(目标方法返回 void 时使用)
private static注入方法通常写成私有静态/实例方法,避免被当作目标类方法使用

启动游戏,你会在控制台看到:

Injected to Vars::finishLaunch by example mod.

这正是 mod-templete 中的官方示例。

@Mixin 声明”我要修改谁”。三种写法:

// 写法 1:目标类编译期可见(推荐,有类型检查)
@Mixin(Vars.class)
public abstract class MyPatch {}
// 写法 2:目标类不可见(内部类、包私有类),用字符串全限定名
@Mixin(targets = "mindustry.ui.dialogs.ContentInfoDialog$ContentTable")
public abstract class InnerPatch {}
// 写法 3:同时注入多个类
@Mixin({Vars.class, Core.class})
public abstract class MultiPatch {}
参数类型默认值说明
valueClass<?>[]{}目标类(编译期可见时用)
targetsString[]{}目标类全限定名(内部类、不可见类)
priorityint1000优先级,数值越小越先应用(用于多个 Mixin 冲突时)
remapbooleanfalse混淆映射开关。Mindustry 无需混淆,保持默认 false 即可

@Inject 是最常用的注解:把一段代码插入目标方法的指定位置。

位置含义示例
HEAD方法开头@At("HEAD")
RETURN每个 return 语句之前(含隐式 return)@At("RETURN")
TAIL方法末尾(最后一个返回点)@At("TAIL")
INVOKE某个方法调用之前@At(value = "INVOKE", target = "Lmindustry/Vars;foo()V")
CONSTANT某个常量出现处@At(value = "CONSTANT", args = "intValue=0")

注入方法必须带一个回调参数,根据目标方法的返回值选择:

目标方法返回回调参数
voidCallbackInfo ci
非 voidCallbackInfoReturnable<返回类型> cir
// 目标:public int getBuildCost(Item item)
@Inject(method = "getBuildCost", at = @At("RETURN"))
private void patchCost(CallbackInfoReturnable<Integer> cir) {
int cost = cir.getReturnValue();
cir.setReturnValue(cost + 1); // 修改返回值
}
@Inject(method = "update", at = @At("HEAD"), cancellable = true)
private void cancelUpdate(CallbackInfo ci) {
if (Vars.state.isPaused()) {
ci.cancel(); // 取消目标方法的后续执行
}
}

method 参数可以是:

  • 方法名:"finishLaunch"(有重载时需带描述符,见下);
  • 方法名+描述符:"getBuildCost(Lmindustry/type/Item;)I"——有多个重载时用描述符精确指定;
  • <init>(构造器)、<clinit>(静态初始化块)等特殊名称。

@Shadow 让 Mixin 能”借用”目标类的私有字段和方法:

@Mixin(Vars.class)
public abstract class VarsPatch {
// 目标:private static final int someCounter
@Shadow
private static int someCounter;
// 目标:private void doSecret()
@Shadow
private static void doSecret() {}
}

要点:

  • 影子字段/方法的签名必须与目标一致(名称、类型、参数);
  • 影子方法写成 abstract(方法体无所谓,应用时会被替换);
  • 影子字段以读取为主:直接给影子字段赋值要谨慎——目标字段若是 final,必须加 @Mutable 才能写(见注解参考手册),而且目标类代码随时可能重置你写入的值;
  • 想给目标实例附加自己的状态时,别去改写目标字段,用 @Unique 声明自己的新字段更稳妥(见下)。

先说明:Mixin 类里声明的普通字段/方法(不加任何注解),应用时会被合并进目标类,成为目标类的新成员——“给目标类新增成员”本来就是 Mixin 的默认行为:

@Mixin(SomeClass.class)
public abstract class Patch {
private int myCounter; // 合并进目标类,成为新字段
private void helper() {} // 合并进目标类,成为新方法
}

但普通成员是”覆盖层”式的,遇到重名很危险:

  • 方法与目标方法同名同签名 → 会直接覆写目标方法。非混淆环境下不加 @Overwrite 也会覆写(见完全覆写 @Overwrite),可能悄悄改掉游戏原有逻辑;
  • 字段与目标字段同名 → 字段无法覆写,会被丢弃或直接报错。

所以普通成员只适合”目标类里肯定没有”的新名字。想安全地新增成员,并让重名冲突按规则自动处理,就给成员加上 @Unique(见下节)。

@Unique 声明该成员绝对不会覆写目标类的任何成员——它只是 Mixin 自己的新成员。应用时这些成员仍会照常合并进目标类(其他 Mixin 也可用 @Shadow 引用);但与目标类已有成员重名时,Mixin 不会去覆写目标实现,而是按可见性自动处理冲突(详见注解参考手册):

@Mixin(SomeClass.class)
public abstract class Patch {
@Unique
private int myCounter; // 新增字段:合并进目标类
@Unique
private void helper() {} // 新增方法:合并进目标类
}
可见性与目标类成员重名时的行为
private / protected自动重命名(如 helper$md5abc...),功能保留
public直接被丢弃,并打印 WARNING(@Unique(silent = true) 可抑制)

要点:

  • 私有/保护成员重名会自动重命名,一般不用刻意避开目标类的成员名;
  • 公有成员重名会被丢弃,命名时仍需避开目标类的既有成员名;
  • 给目标类”新增”字段/方法时推荐用它:目标类代码不会重置你的新字段,比 @Shadow 改写目标字段更稳妥;
  • 也可以把 @Unique 标在类上,让类里所有成员默认都视为 @Unique。

@Accessor / @Invoker 用接口的方式,为目标类的私有字段/方法生成”梯子”:

@Mixin(SomeClass.class)
public interface SomeClassAccessor {
@Accessor("privateField")
int getPrivateField();
@Accessor("privateField")
void setPrivateField(int value);
@Invoker("privateMethod")
void callPrivateMethod();
}

使用时把它当成普通接口:

SomeClassAccessor acc = (SomeClassAccessor) someInstance;
int v = acc.getPrivateField();
  • 访问器方法名按 JavaBean 约定自动匹配字段(getXxx/setXxx),名字对不上时用 @Accessor("字段名") 显式指定;
  • @Invoker 同理:@Invoker("方法名"),方法签名需与目标方法匹配;
  • 纯访问器 Mixin(只含 @Accessor/@Invoker)写成 interface,实现类由 Mixin 自动生成。

@Overwrite 用 Mixin 中的方法整体替换目标方法:

@Mixin(Vars.class)
public abstract class VarsPatch {
@Overwrite
public static void finishLaunch() {
Log.info("replaced finishLaunch!");
}
}

谨慎使用:

  • 无法看到目标方法的原始实现,一旦游戏更新改动了签名/行为,你的覆写可能破坏游戏;
  • 与其他 Mixin 的 @Inject 等注入冲突;
  • 能改用 @Inject 就不要用 @Overwrite。

实际上,在非混淆环境(如 Mindustry)中,只要方法签名匹配,Mixin 中的同名方法默认就会覆写目标方法——@Overwrite 主要是一个显式声明(并让注解处理器检查)。

Mixin 的目标不限于游戏本体,也可以是其他已安装的模组:

"mixins": {
"mindustry:new-horizon": "mixins/nh.json",
"test:testmod": "mixins/copper-mod.json"
}

游戏不同版本内部实现可能不同。Mixin 配置支持按目标版本选择要应用的 Mixin 类(详见配置 JSON 详解):

{
"version": 1,
"config": {
"mixins": {
"*": ["CommonPatch"],
"<8.0.27179": ["OldVersionPatch"],
">=8.159": ["NewVersionPatch"]
}
}
}
Terminal window
java -jar CopperLoader.jar -G mindustry.jar --mixin-log example:examplemod

--mixin-log <模组id> 为该模组启用审计日志,输出每个 Mixin 的应用情况。

Terminal window
java -jar CopperLoader.jar -G mindustry.jar --mixin-flag example:examplemod,DEBUG_EXPORT
  • --mixin-flag <模组id,标志1,标志2,...> 设置 Mixin 环境标志;
  • DEBUG_EXPORT 会把注入后的类写到工作目录的 .mixin.out/<容器id>/ 下(可用反编译工具查看);
  • 其他可用标志见 org.spongepowered.asm.mixin.MixinEnvironment.Option。

调试模组时,用 --mod-debug-jar 与 --mod-debug-classpath 指向已安装的 Jar 与编译输出,就能不重启游戏看到改动效果(热交换):

Terminal window
java -jar CopperLoader.jar -G mindustry.jar \
--mod-debug-jar .mindustry/copper/mods/example-examplemod.jar \
--mod-debug-classpath build/classes/java/main

改完重新编译,改动就会生效。什么是热交换、有哪些限制,见加载器命令行 · 调试模组。用官方模板起项目时这条命令已经写在 Debug 运行配置里,打开 IDEA 就能调试,见快速开始 · 调试。

报错含义处理
Unable to resolve mixin ... target class ...找不到目标类检查目标 id/类名是否写对、目标模组是否安装
Invalid signature for ... in ...影子成员签名与目标不一致检查 @Shadow/@Invoker 签名
Critical injection failure ...注入点匹配失败(游戏版本变了)检查方法名/描述符;用版本过滤拆分支
Multiple methods matched ...方法名有重载,无法确定目标补充方法描述符