本帖最后由 liach 于 2019-3-11 07:26 编辑
导言

Coremod 是 Forge 的一种机制,也是模组的一种。与其它模组相比,它们有在 Minecraft 正常启动、Minecraft 和普通模组代码被加载前提前被加载的特权。
它并不等同于修改字节码、ASM 或者 Mixin;这只是它的一种用途。

目的

Coremod 的目的多种多样:
  • 实现对 Minecraft 以及模组源代码的修改;
  • 提取前置模组;
  • 在 Minecraft 加载前对环境进行预处理;
  • 等等。

虽然列表中的第一项是最常用的,但这并不代表第一项是 coremod 唯一的目的
在 1.7.10 左右,神秘时代和工业 2 实验版都有过 coremod 提取前置。神秘时代用 coremod 提取 codechickenlib (还是 baubles,我忘了),而工业 2 提取前置数学库 ejml。
在 1.9 以前几乎是 Minecraft 中文玩家必备的 InputFix 则同时使用 coremod 检测操作系统
SpongeForge 的 coremod 会初始化 mixin 系统,以准备马上对 Minecraft 源码的修改。

使用

Forge coremod 系统有几个主要的类:IFMLLoadingPlugin、IFMLCallHook 和 IClassTransformer。大致骨干如下:
  1. public interface IFMLLoadingPlugin {
  2.   @Nullable String[] getASMTransformerClass(); // 返回一个数组,包含本 coremod 的所有实现 IClassTransformer(字节码修改器)的类全名,可返回空数组或者 null
  3.   @Nullable String getModContainerClass(); // 返回一个实现 IModContainer 的类的全名,可返回 null
  4.   @Nullable String getSetupClass(); // 返回一个实现 IFMLCallHook 的类的全名,可返回 null
  5.   void injectData(Map<String, Object> data); // Forge 提供接口,可获得 mcLocation、coremodList、runtimeDeobfuscationEnabled、coremodLocation 几个信息,分别是 File 类型的游戏路径、List<FMLPluginWrapper> 类型的封装后的 coremod 列表、Boolean 类型的是否会在运行时反混淆 Minecraft 本身、File 类型的 coremod 自身路径。
  6.   @Nullable String getAccessTransformerClass(); // 推荐不要使用,用 FMLAT 这个 jar 属性替代。返回一个 AccessTransformer 子类的全名,可返回 null。
  7. }

  8. public interface IFMLCallHook extends Callable<Void> {
  9.   @Override @Nullable Void call() throws Exception; // 执行这个呼叫器的实际逻辑,必须返回 null
  10.   void injectData(Map<String, Object> data); // 同 IFMLLoadingPlugin 的 injectData
  11. }

  12. public interface IClassTransformer {
  13.   byte[] transform(String name, String transformedName, byte[] basicClass); // 在此方法中对 Minecraft 的字节码进行修改,可原封不动返回
  14. }
复制代码
看见 IFMLLoadingPlugin 里面的几个方法都返回字符串了吗?这是为了避免意外加载类导致错误。
injectData 的实际数据可以在这里找到


稍安勿躁!请至少看完注意事项后再开始尝试实战!


注意事项

Coremod 作为一种缺少介绍的东西,本身(不包括 asm 或者修改字节码)使用起来实际上有很多危险注意事项

  • Coremod 不应该加载正常模组的类或者 Minecraft 的类,甚至包括自己的 mod container 类。(特别容易犯的错误,且后果很严重)
  • Coremod 构建出来的 jar 需要额外的属性。
  • Coremod 除了实现接口方法以外,最好添加一些注解信息。
  • Coremod 相关的类都要有一个没有参数的构建器。

暂时想到这么多。会看反馈添加。

一、加载代码隔离
Coremod 不应该加载正常模组的类或者 Minecraft 的类,甚至包括自己的 mod container 类。

加载正常模组以及 minecraft 代码会导致任何 coremod 无法对其进行修改,从而导致游戏崩溃。
注意: 一个 coremod 的 mod container 类是在 Minecraft 正常启动之后才被创建实例的,所以也属于正常模组类而不是 coremod 类!
具体会有什么影响呢?

例一:臭名朝著的 Sponge 与 AE 2 的冲突。

罪魁祸首:(IFMLLoadingPlugin coremod 类直接实现 mod container)
https://github.com/AppliedEnergistics/Applied-Energistics-2/blob/e3bf52ff7b027d9f00d23db860040f7f6cf9af38/src/main/java/appeng/coremod/AppEngCore.java#L73
导致问题:
https://github.com/AppliedEnergistics/Applied-Energistics-2/issues/2482
因为 IModContainer 类被 AE 2 的 coremod 提前读取,导致 Sponge 无法修改 IModContainer 从而崩溃。AE 在拖了很久以后才修复。

顺便一提,给 coremod 类实现 IFMLLoadingPlugin 的同时实现 IModContainer 并无任何优点。详见底下的第四小段。

例二、MicdooodleCore(星系 mod 前置)的 coremod 加载 Forge 处理模组版本信息的 VersionRange 类

罪魁祸首:
MicdoodleCore 加载了正常 Forge 代码中用来检测软件版本的类,而这个软件版本检测类用到 Minecraft 实现翻译功能的正常代码。

导致问题和 u.s.knowledge 给出的合理解释:

https://github.com/micdoodle8/MicdoodleCore/issues/10#issuecomment-342400911
用来检测软件版本的类被提前加载,所以里面对被混淆的 Minecraft 代码的引用没被修改,导致游戏崩溃。


如何避免这类问题呢?一般大家就把 coremod 内容和普通 mod 的内容放到不同的包下;然而,还是不能避免不小心跨包 import 等现象。

这里就举个自己想到的解决方案作为正面教材:利用 Gradle 构建系统提供的 source set 进行将 coremod 和普通 mod 代码隔离,避免互相引用。

https://github.com/liachmodded/datapacks/blob/e835ee7a3ef2e8492e56ebadef5a59be2c5d7c6e/build.gradle#L73-L80
上面 coremod 的定义采用了 main source set 的编译依赖从而可以使用 forge 的 coremod 接口代码;main 在运行的时候同时运行 coremod 的代码,这样测试运行时 coremod 也会被测试。
然而,这只能隔离普通 mod 的代码。要避免误用 Minecraft 代码,还需要编写者的细心。

二、jar 属性
Forge 识别的模组 jar 属性中有四个跟 coremod 相关,如下。
https://github.com/AlmuraDev/Almura/blob/36866cdcd7bb0722aa8276d8e1ed96559fbb67f9/build.gradle#L190-L193

有 FMLAT、FMLCorePlugin、FMLCorePluginContainsFMLMod、ForceLoadAsMod 这四个属性。

FMLAT 代替了一部分 coremod 功能,能够让模组自定义 access transformer 修改原版代码中方法和字段的可见性而不需从 coremod 中定义。Access transformer 作为源代码的一种修改,本身和 coremod 联系不大,所以这里不会多讲。

FMLCorePlugin 是必备的;它是 Forge 检测 coremod 的唯一途径。这里指向的类用 . 而不是 / 分隔包,同时这个类必须实现 IFMLLoadingPlugin 接口(并且要有一个没有参数的构建器)。

FMLCorePluginContainsFMLMod 设为 true 是表示一个 jar 内除了 coremod 外有普通的用 Mod 注解的模组。
ForceLoadAsMod 一般和 FMLCorePluginContainsFMLMod 同时设为 true 以保证 jar 中的普通模组被正常读取,虽然实际用途我未能询证到(GitHub 默认分支改变,1.12 的老内容无法查询了)。

这些 jar 属性最重要的是在编译环境运行时如果有错完全不会显现;只有将构建的 jar 测试后才能确定到底有没有问题。

三、注解信息

Forge 给 coremod 提供了五个可选的注解。
https://github.com/MinecraftForge/MinecraftForge/blob/e6fbf39591b920532ea464d36dad24671c6d08fd/src/main/java/net/minecraftforge/fml/relauncher/IFMLLoadingPlugin.java#L83-L144
这五个分别是 TransformerExclusions、MCVersion、Name、DependsOn、SortingIndex,都是 IFMLLoadingPlugin 的子类。这几个注解都是针对类的,只要直接放在你的 coremod 类上就行。

TransformerExclusions:推荐添加。给 coremod 加载器(实际上是背后的 LaunchWrapper)提供一个包名单,避免这些包里面的内容被其他 coremod 修改。可以避免其他 coremod 误改你的 coremod 内容。

MCVersion:推荐添加。指定针对的 Minecraft 版本。用来保证 coremod 安全性,因为不同版本的 Minecraft 代码一般相差很大。

Name:推荐添加。给这个 coremod 起个名字,到时候崩溃报告里面就会用这个自定义的名字表示你的 coremod。

DependsOn:直接忽略。理论上是让 coremod 在前置存在的时候才加载,但是实际上这个功能从来没有实现过。

SortingIndex:按需添加。定义了 coremod 的读取顺序;越小优先级越高。未定义则默认为 0。


四、无参构建器

Forge coremod 系统中,实现 IFMLLoadingPlugin 的类和其接口实现中返回名称的实例类都需要有无参数公开构建器。
Forge 在实际使用中,会直接通过反射创建这些类的一个实例,然后在创建的实例上呼叫接口方法。


这些被实现的接口有 IFMLLoadingPlugin、IFMLCallHook、IClassTransformer 和 IModContainer。

顺便一提,给 coremod 类实现 IFMLLoadingPlugin 的同时实现 IModContainer 并无任何优点

原理就是这个 coremod 类如此以来就会有两个实例,一个会被当做 IFMLLoadingPlugin 保存,另一个会被当做 IModContainer 保存。
刚刚 AE 2 负面教材中的 coremod 的 log 就会录两次,metadata 也会被准备两次,得不偿失。




结语


这篇文章旨在给大家介绍一下 coremod 的本身,打破大家认为 coremod 等同于修改字节码的成见等。
很抱歉,本帖并不介绍 ASM、Access transformer 或者 mixin 等字节码修改工具,为的是能让大家更好理清这两种不同的概念。


友链
感谢 @森林蝙蝠 和 @u.s.knowledge 与我探讨 coremod 话题。

另外森林蝙蝠有篇介绍 coremod 但是跑题了的文章。虽然没有很多关于 coremod 本质的内容,但是大篇幅介绍了字节码的修改。http://www.mcbbs.net/thread-822754-1-2.html