MinecraftForge 由十数个项目组合而成,本文对此做一个简单的介绍。

这篇文章以 Minecraft 1.19.2,Forge 43.1.0 为例进行分析。

你可以在这里清晰明了的看到 Forge 的所有项目。

Forge 本体

Forge 可以简单地定义为一个 Mod 加载器和一套帮助 Mod 兼容的 API。后者,也就是这套 API 并不是本文关注的重点,虽然不可避免地会讲到一些。

回想一下你是如何安装 Mod 的,就可以很容易地总结出 Mod 加载器是干什么的:把 mods 文件夹里的 JAR 文件加载到游戏里。

编译期

Forge 的开发工作依赖于一系列项目。

ForgeFlower

这是一个 Java 反编译器。

Forge 项目对 FernFlower 的修改,修复了一些怪异和错误的编译结果,并且增加了多线程反编译的支持,因此比其他(比如 Spigot)的反编译快了许多。

MCPConfig

MCP 项目唯一的遗产。MCPConfig 用于提供包括:

  • 混淆名称到 srg 名的映射,用 TSRG 格式的文件保存
  • 反编译后的代码文件的修复,用 patch 文件保存

在内的功能。

MCP 映射表格式

MCP 项目同时带有一个映射表,是社区维护的,当然现在已经停止维护了。

这个映射表导出之后,是一个 zip 压缩包,里面可以含有包含方法、字段、参数的 csv 文件。

例如,ForgeGradle 生成的 1.19.2 官方映射表格式为:

mapping-1.19.2-official.zip
  | fields.csv
  \ methods.csv

csv 文件格式类似:

searge,name,side,desc
m_100003_,lambda$init$1,0,
m_100013_,updateList,0,

含有 searge 名,翻译名,仅客户端/服务端的信息和 Javadoc 注释。

MappingVerifier

检查一个映射表会不会破坏继承关系。

这里的破坏是什么呢?很简单,如果调用一个方法,被 remap 之后最终调用的实现不一样,继承关系就被破坏了。

考虑以下情况:

abstract class A {
    public void foo() {
        System.out.println("A");
    }
}

interface B {
    void foo();
}

class C extends A implements B {
}

此时接口 B 的 foo() 通过子类 C 的父类 A 间接实现了;而如果一个映射错误的把 A.foo()B.foo() 分配了两个不一样的名字,那么调用 B.foo() 就不会正确的调用 A 中的实现,继承被破坏了。

当然这是最为复杂的一种情形,也会有把 String foo() 映射成 String toString() 这样错误覆盖了继承等等。

SpecialSource

https://github.com/md-5/SpecialSource

一个根据某个映射表对 JAR 进行 remap 的工具。

基本上用法就是

java -jar SpecialSource.jar -i in.jar -o out.jar -m mappings.srg

这个工具是 md-5 写给 Spigot 项目的,Forge 一直用到了大概 2021 年。

同类型工具里支持格式大概是最多的,也很好用。

ForgeAutoRenamingTool

平替 SpecialSource 的工具。这个名字的缩写大概也是 LexManos 的恶趣味。large lols

能平替 SpecialSource 的工具还有 CadixDev 的 Vignette,Forge 似乎有过用这个的意愿,后来又换掉了。

SrgUtils

对映射表这个概念做抽象形成的类库,也有 remap 的功能。

其实不是那么好用,比较好用的是 CadixDev 的 Lorenz

Srg2Source

前面的 SpecialSource 等都是对已经编译过的 .class 文件进行 remap,这个项目是对 Java 源文件进行 remap。

项目是基于 Eclipse JDT 项目开发的,基本可以算作一个修改过的编译器。

比较不好用,因为要生成一个 RangeMap 用于存储需要 remap 的符号的位置。比较好用的还是 CadixDev 那边的 Mercury,不需要生成中间文件。

artifactural

一个支持 Artifact Transform 的 Gradle 插件。

什么意思呢?在 Gradle 5.5 发布之前,Gradle 并不支持对依赖进行例如反混淆的修改。这个插件提供了动态生成和修改一个依赖的功能,Forge 用来生成 MCPConfig 处理过的 MC Jar 和经过反混淆的依赖。

内部的实现方式应该是实现了一个 Repository 之类的接口。

Forge 似乎有过迁移到 Gradle Artifact Transform 这套系统上,但是没有后话了。

AccessTransformers

编译器(和运行时)修改字段和方法访问级别的工具。

唯一不好的一点就是修改了 AT 文件之后需要重新反编译。

JarCompatibilityChecker

检查两个 JAR 文件之间的字节码兼容,也就是不会有 LinkageError 和 IncompatibleClassChangeError 之类的错误,现在用来检验 AT 有效性。

可能是 Forge 为引入更多编译期字节码变换(接口注入?)做的准备。

ForgeGradle

Forge 的 Gradle 插件。

使用了上面的项目,帮助开发者引入反编译反混淆后的 Minecraft 依赖。

运行时

bootstraplauncher (BSL)

引导 modlauncher 的前置项目,基本上就是把 modlauncher 需要的依赖全部加载成 Java 模块,然后调用 modlauncher 主类。

securejarhandler (SJH)

提供了多个功能的类库。

提供 JAR 签名验证支持。Java 自带的 zipfs 不支持 JAR 签名验证(毕竟不是 jarfs),但是 cpw 等人认为需要支持这个功能。最终的实现是反射调用 JarFile 的内部签名验证实现。

为 JPMS 提供一个专用的类加载器,主要的类是 ModuleClassLoader。

提供一个 UnionFileSystem,可以把多个文件系统组合在一起,应该就是模仿 Docker 使用的文件系统。

modlauncher (ML)

模组加载器,提供在类加载期进行字节码增强功能,同时也负责 Minecraft 的引导。理论上来说,ML 支持运行任何 Java 程序。

modlauncher 下一共有四个 ModuleLayer,分别是:

  • BOOT,也就是 BSL 里的那个模块。
  • PLUGIN,供一个 ILaunchPluginService 接口作为 Java SPI 使用,暴露给 Forge 自身。AccessTransformer 和 Mixin 之类的东西都是以这个实现功能。
  • SERVICE,主要供 ITransformationServiceIModLocatorIDependencyLocator 接口使用,暴露给 Modder,可以使用这个 Layer 进行字节码增强操作。
  • GAME,运行 Minecraft 和模组的 Layer,唯一一个支持字节码被增强的。

其中 PLUGIN 和 SERVICE 都只能访问自身和 BOOT 中的类,GAME 可以访问所有的类,以此进行类加载隔离。

Launch target 这个概念也是在 ML 里提供的,是一个 ILaunchHandlerService SPI,在 Forge 中用来启动不同的环境,比如客户端、服务端、GameTest、Data Generator 的开发环境和生产环境。

ML 是在 Minecraft 1.13 Forge 工具链大更新时期诞生的项目,解决了 LaunchWrapper 不支持 Java 9 以上版本的问题。

2020 年(Minecraft 1.17)以前,ML 并没有集成 JPMS 支持,也没有对 LaunchPlugin 和 Transformer 进行隔离。后来的 SJH 项目实际上是从 ML 拆分出去的。

JarJar

实现了 Jar in Jar 文件系统,用于 Forge Mod 的依赖加载。

实际上实现没有特别复杂,是基于路径进行转换再代理到 zipfs。

coremods

实现了一套基于 Nashorn JS 的字节码修改系统。

这套系统解决了 1.12 时代不小心在 coremod 类里引用 Minecraft 类的问题,因为对类加载做了严格隔离。使用这套系统的 JS 通过类加载器限制只能引用指定的几个 ASM 类和指定的几个基础 Java 包。

就是太难用了,也没有补全。

在 ModLauncher 升级到 JPMS 和几个 ModuleLayer 的隔离机制后,这套系统的初衷也不成立了。

eventbus

实现了 Forge 的事件总线。

具体实现是基于 ASM 生成调用类,性能比较好。

这套系统的实现非常依赖 context classloader,如果没有设置就会有一些莫名其妙的 NoClassDefFoundError。这套系统也只能在 GAME Layer 的那个类加载器环境里使用,如果模组创建了一个子类加载器,也是不能用的。

在我看来完全可以迁移到 MethodHandles.Lookup 的 defineHiddenClass 去。

拓展阅读

我在另一篇文章里写了一些关于服务端的实现原理。