在Forge端是Forge模组,在Fabric端是Fabric模组
(资料图)
本教程适用于至少使用过Forge或Fabric开发环境(之一)的人。同时需要涉及到一些gradle知识。
(教程见置顶评论)
TL;DR
见置顶评论
Forge和Fabric的区别,以及为什么他们可以兼容
任何一个有一定时长的mod玩家都知道一个常识,那就是Forge和Fabric是不能兼容的,他们不能装在同一个客户端上。这是因为Forge和Fabric对游戏代码的修改都非常复杂,同时应用二者的修改难以避免冲突。但是对于mod作者而言,想要制作一个同时兼容Forge和Fabric端的模组则有完全不同的挑战。这是因为Forge和Fabric端的模组加载方式有根本上的区别。
Forge模组是如何加载的
Forge在启动时会扫描所有class文件,并且从中选取拥有 @Mod 注解的类作为模组的主类,以主类作为入口点启动mod程序。 Forge还提供一个通过 META-INF/coremods.json 文件加载js mixin的功能。
Fabric模组是如何加载的
Fabric在启动时会读取所有mod的 fabric.mod.json 文件,并根据其内容加载mod。其中包含Fabric模组的主类和mixin类,还支持根据可选依赖mod加载对应的接口类。总而言之,所有Fabric模组加载的东西都能直接或间接地从 fabric.mod.mixin 文件中找到。
如何让两个mod兼容
我们知道Java是在需要使用一个类的时候才会加载这个类的,因此只要我们在Forge端不加载Fabric相关的类、在Fabric端不加载Forge相关的类就能兼容了。具体来说,就是 @Mod 注解的类(和其他涉及Forge API的类)不要在 fabric.mod.json 直接或间接引用,反过来也一样。
一般来说,我们可以把所有代码分为三部分:涉及Forge的部分、涉及Fabric的部分、通用的部分。Forge部分和Fabric部分可以调用通用部分,而通用部分不能调用另外两部分。我们尽量压缩Forge和Fabric的部分,只把直接和他们相关的代码放在这里。如果确实有通用部分调用Forge/Fabric功能的需求,我们可以在通用部分添加一个接口,让Forge/Fabric部分主动提供对应的实现(比如放到通用部分里的一个静态字段里),间接调用通用部分。
如何编译一个兼容的mod
我们现在已经知道一个兼容mod长什么样了,但是要怎么编译出一个这样的mod呢?这就要求我们知道mod编译的流程了。Fabric和Forge两个框架的mod编译都是使用gradle的:
Forge编译流程
Forge编译总的来说是先编译Java类,再将编译结果和资源文件放到一起打包,最后将打包结果反混淆。
Fabric编译流程
Fabric编译总的来说是先编译Java类,再将编译结果和资源文件放到一起打包,最后将打包结果反混淆。
还真不一样
Forge和Fabric使用的混淆方式不同,Forge在开发时会使用MCP名(似乎现在改成官方反混淆名了),而运行时会使用Srg名;Fabric在开发时会使用yarn名,在运行时会使用intermediary名。如果你的代码当中有涉及到minecraft的类、字段或方法,那么Forge和Fabric在运行时的叫法是不同的。
那怎么办呢
我们可以先编译出来,让Fabric和Forge分别反混淆一次。。但是这样做有一定的风险,MCP名和yarn名都使用自然语言取名,难免有冲突的可能。如果有一个类的MCP名和yarn名一样,那么它在Fabric和Forge上都会被反混淆,最终得到的结果就是不确定的了。这种方式还有一个问题,就是Forge官方的编译插件和Fabric官方的编译插件不兼容,修补起来很麻烦。
我最终采取的做法是让三个模块(forge、fabric、common)分别编译打包反混淆,都打包出最终结果后再将其拆包合并到一个最终产物上。具体做法:
allprojects {
tasks.register('feedbackClass', Copy) {
dependsOn "jar"
dependsOn ":forge:reobfJar"
dependsOn ":fabric:remapJar"
from (zipTree(new File(buildDir, "libs/${project.name}.jar"))) {
include '**/*.class'
}
into new File(rootProject.buildDir, 'classes/java/main')
}
tasks.register('feedbackResource', Copy) {
dependsOn "jar"
dependsOn ":forge:reobfJar"
dependsOn ":fabric:remapJar"
from (zipTree(new File(buildDir, "libs/${project.name}.jar"))) {
exclude '**/*.class'
}
into new File(rootProject.buildDir, 'resources/main')
}
tasks.register('feedback') {
dependsOn("feedbackClass")
dependsOn("feedbackResource")
}}
project.jar {
dependsOn ':common:feedback'
dependsOn ':forge:feedback'
dependsOn ':fabric:feedback'
}
这里给每个子模块都定义了一个feedback任务,其中包含处理java类的feedbackClass和处理资源文件的feedbackResource。feedback任务将打包好的文件解包复制到主模块的构建目录下。每个子任务都在forge和fabric反混淆完成之后执行,主模块只要在三个feedback任务之后再打包jar就可以了。
最后
以上给出了编译一个forge/fabric兼容mod的原理和核心代码,完整实现还需要进一步的工作,包括为forge和fabric模块进行进一步的配置和优化。这部分可以参考我的mod,或者下载TLDR中的示例代码。注意我的mod和示例代码都没有使用forge推荐的反混淆表,因为Mojang官方提供的反混淆表存在法律问题 。如果你已知晓风险并仍然希望使用官方反混淆表,可以将
mapping_channel=snapshot
mapping_version=20210309-1.16.5
改成
mapping_channel=official
mapping_version=1.20