1. 起因:震惊!朋友亲手整合的Minecraft Java版整合包由于Mod数量太多,新手玩家居然完全Hold不住…
朋友自己组了一个MC整合包,足足有310个模组,模组加载器初始化后340个,基本涵盖了MC比较好玩和经典的模组。经过测试,整合包能够正常地在windows和macOS上跑起来,我们前期打算边玩边测试,有问题再慢慢调。
玩MC肯定要联机,我们偷懒地直接通过内网穿透的方式进行远程联机,玩了几天后发现还是有些不方便,这种联机意味着作为服务器的一方要随时开着游戏才能保证另一方想玩的时候就玩,但是我们不想改变现状,耗费了约几秒钟提出了以下补偿方案:
- 主机方7✖️24小时开着自己电脑
- 其他玩家需要玩的时候主机方随时响应
- 使用类似坚果云的同步工具同步存档
- …
经过研究,上述方案几乎全部拉垮
(废案)1. 主机方7✖️24小时开着自己电脑注:7x24小时挖矿它不香吗?
(废案)2. 其他玩家需要玩的时候主机方随时响应注:技术可以实现(远程触发WOL + 开机自启服务之类的操作),人工也可以实现(接到微信消息打开游戏),当事人不愿意体验下班oncall遂放弃。
(废案)3. 使用类似坚果云的同步工具同步存档注:表面上看,方案可行,但暗藏个人存档数据覆盖本地存档的诸多问题,比如同步存档后你登录后可能是其他主机的角色,需要进一步研究server.dat文件,实际操作层面是在挖更多坑。
2. 稍微操作一下:直接将整合包迁移到Minecraft Forge Server不香吗?
利用现有资源快速部署
整合包在本地大概需要至少4~6G左右内存,之前搞的一个4U8G云服务器跑一个Minecraft问题应该不大。
综上所述,直接在云服务器部署一个MC服务器反而最舒服,MC如何开服这种问题就不用细说了,各大搜索引擎大量这类内容。我部署了[minecraft forge 1.12.2 - 14.23.5.2855 + Minecraft Server1.12.2版本](Downloads for Minecraft Forge for Minecraft 1.12.2)
然后就是导入整合包的模组,再运行发现失败了,查看崩溃日志发现有一个Mod异常,通过检索资料了解到,该mod只需要在客户端安装就行,所以我移除了该Mod,然而继续运行仍然崩溃。
cp -r /your/pkg/.minecraft/mods /your/path/mods
再仔细查看日志,找到当中建议移除的40多个“可能引起异常”Mod,二话不说直接把提及的Mod都移除了,不过结果并不美满,forge server依旧直接崩溃了。
不香,至少不是开箱即用的方案
- 部署虽然失败了,原因很清楚:某些模组无法在服务器上运行;对此,我得到如下两个解决方案:
- 反复运行forge服务器(每次完整加载所有340个模组需要6~10分钟),查看每次产生的crash-logs,并根据出现的exception移除对应模组;
- mods目录初始为空,每次添加一部分整合包的模组进来,然后反复运行,出现exception的模组就不往mods目录加;
- 显然,上述两种方案,操作不难,却相当麻烦;作为一名新手玩家,我不打算一次性搞懂每个模组的功能,但是我打算进一步研究Minecraft模组结构,看看是否能得到一些帮助。
3. 折腾:研究模组之间依赖关系
对于新手玩家可能有许多困惑
- 各种模组加载器有什么不同?例如forge,fabric等
- 模组加载器加载Mod的流程是怎么样的?
- 怎么样通过一个jar包了解到Mod的具体信息?对于FML是怎么做的?
- FML每次启动都报这么多error和warning,为什么最终还是能启动,并且模组功能基本也没有什么影响?
- …
- 以上困惑也是我的困惑,我们知道Mod基本都是一个jar包的形式,从直接观察的情况来看,模组加载器读取了jar包后,能够验证Mod适配的MC版本,能够了解Mod的依赖其他Mod的信息,能够读取版本和作者等基础信息;所以现在我们需要做的就是去分析模组加载器是如何得到这些信息的。
直接阅读对照两个Mod的源码
Mod基本上都是开源的,这里我选了两个有依赖关系的模组:更多鸡(chickens)和更多鸡扩展(more chickens),chickens模组是在原版MC的基础上增加的鸡种群和产物,more chicken是在chickens模组的基础上再次扩展种群和产物,也就是说more chickens依赖chickens模组
# 对应Minecraft 1.12.2版本 Chickens: 6.1.0 MoreChickens: 3.2.0
如果依赖不正确,模组加载器会提示required异常,如下图所示
阅读more chickens源码,发现一些依赖chickens的迹象
com.gendeathrow.morechickens.core.ChickensMore.java确实引入了chickens注册鸡实体的相关类
import com.setycz.chickens.registry.ChickensRegistry; import com.setycz.chickens.registry.ChickensRegistryItem;
build.gradle中dependencies里确实声明了chickens模组的依赖
dependencies { deobfCompile "chickens:chickens:${chickens_version}" ... }
gradle.properties文件里给出了chickens的相应版本,这个版本号与前面MC运行时提示的版本号是一致的
# minecraft 版本 minecraftversion=1.12 # chickens模组版本 chickens_version=6.0.2
结论是我们能够根据源码的项目依赖轻松获取各种静态依赖信息,然而jar包是编译好的,并且不是所有Mod都是开源的,这些情况如何获取Mod信息;当然,我们可以针对jar包做静态依赖分析,分析目标模组jar包需要依赖哪些jar包,先不说结果是否一直,我想问模组加载器也是这么干的吗?
研究Forge模组加载器(Forge Mod Loader, FML)的加载行为
分析MinecraftForge 1.12.x源码中FML
net.minecraftforge.fml
部分首先,如下图,我们先看一下MinecraftForge启动后,FML做了什么事情
从上图可以看出,FML在完成一系列初始化配置后,最终在第五步
identifyMods
这个方法中完成了Mod的识别,所以接下来我们进一步看FML到底是如何识别Mod的,如下图我们现在可以了解到FML实际上通过一个
JarDiscoverer
类的discover
方法实现了jar包的解析,我们通过源码继续解读分析// class JarDiscoverer implements ITypeDiscoverer // discover方法 // 最终返回ModContainer类,ModContainer是一个interface @Override public List<ModContainer> discover(ModCandidate candidate, ASMDataTable table) { // 省略... try (JarFile jar = new JarFile(candidate.getModContainer())) { // 读取mcmod.info文件 ZipEntry modInfo = jar.getEntry("mcmod.info"); // 省略... // 读取json文件 findClassesJSON(candidate, table, jar, foundMods, mc); // 省略... // 读取class字节码 findClassesASM(candidate, table, jar, foundMods, mc); } // ... }
discover
方法使用try with resources
尝试读取了jar包中的mcmod.info文件,并且还继续调用另两个方法findClassesJSON
和findClassesASM
读取文件,我们继续往下看findClassesASM
显然是读取**.class**文件字节码的private void findClassesASM(...) { // 省略... // 遍历匹配jar.entries()中的每个文件,匹配 Matcher match = classFile.matcher(ze.getName()); if (match.matches()) { // 省略... ASMModParser modParser = new ASMModParser(inputStream); modParser.validate(); modParser.sendToTable(table, candidate); ModContainer container = ModContainerFactory.instance().build(modParser, candidate.getModContainer(), candidate); // 省略... // 解析完成,添加ModContainer实例到已发现Mod的List中 foundMods.add(container); // 省略... } }
findClassesJSON
负责读取JSON文件private void findClassesJSON(...) throws IOException { // 读取META-INF/fml_cache_annotation.json文件 ZipEntry json = jar.getEntry(JsonAnnotationLoader.ANNOTATION_JSON); Multimap<String, ASMData> annos = JsonAnnotationLoader.loadJson(jar.getInputStream(json), candidate, table); // 根据上面读取到annos提供的class数据,遍历生成ModContainer for (...) { for (ASMData data : annos.get(type.getClassName())) { // ... ModContainer ret = ctr.newInstance(data.getClassName(), candidate, data.getAnnotationInfo()); // 添加ModContainer实例到已发现Mod的List中 foundMods.add(ret); } } }
完成上面的分析后我们了解几件事情
- 需要读取jar包中的mcmod.info文件(如果文件存在的话),以获取mod基本信息;
- 需要读取META-INF/fml_cache_annotation.json文件,获取class信息;
- 如果没有META-INF/fml_cache_annotation.json文件则直接通过ASM加载class文件字分析类的信息;
具体数据格式在下一节说明
4. 试着做一个小工具
入口
经过源码分析,我们基本了解了MinecraftForge是如何识别Mod的,其中Mod(jar包)中关键的文件有下面几种:
- mcmod.info文件,格式形如:
[ { "modid": "morechickens", "name": "More Chickens", "description": "Adds Tinkers Construct and Draconic Evolution chickens...", "version": "3.2.0", "mcversion": "1.12.2", "url": "https://minecraft.curseforge.com/projects/more-chickens", "updateUrl": "", "authorList": [ "GenDeathrow" ], "credits": "Chickens Mod, GameWalker, ACGaming, MrAmericanMike", "logoFile": "", "screenshots": [], "dependencies": [ "Chickens Mod" ] } ]
- META-INF/fml_cache_annotation.json文件,格式形如:
{ "com/gendeathrow/morechickens/core/ChickensMore": {...}, "com/gendeathrow/morechickens/core/ModItems": {...}, "com/gendeathrow/morechickens/handlers/EggTooltips": {...}, "com/gendeathrow/morechickens/handlers/SpecialChickenHandler": {...}, "com/gendeathrow/morechickens/modHelper/ActuallyAdditionsAddon": {...}, "com/gendeathrow/morechickens/modHelper/BotaniaAddon": {...}, "com/gendeathrow/morechickens/modHelper/ExtraUtilitiesAddon": {...}, "com/gendeathrow/morechickens/modHelper/RefinedStorageAddon": {...} }
展开后如下
{ "com/gendeathrow/morechickens/core/ChickensMore": { "name": "com/gendeathrow/morechickens/core/ChickensMore", "annotations": [ { "type": "CLASS", "name": "Lnet/minecraftforge/fml/common/Mod;", "target": "com/gendeathrow/morechickens/core/ChickensMore", "values": { "acceptedMinecraftVersions": { "value": "[1.12.2]" }, "dependencies": { "value": "required-after:chickens@[6.1.0,)..." }, "modid": { "value": "morechickens" }, "name": { "value": "More Chickens" }, "version": { "value": "3.2.0" } } }, { "type": "FIELD", "name": "Lnet/minecraftforge/fml/common/Mod$Instance;", "target": "instance", "value": { "value": "morechickens" } }, ] } }
- java class文件,经过gradle构建后生成的class文件
1, com/gendeathrow/morechickens/core/ChickensMore.class 2, com/gendeathrow/morechickens/core/configs/JsonConfig.class 3, com/gendeathrow/morechickens/core/configs/MoreChickenConfig.class 4, com/gendeathrow/morechickens/core/ModItems.class 5, com/gendeathrow/morechickens/core/proxies/ClientProxy.class 6, com/gendeathrow/morechickens/core/proxies/CommonProxy.class 7, com/gendeathrow/morechickens/handlers/EggTooltips.class 8, com/gendeathrow/morechickens/handlers/SpecialChickenHandler.class 9, com/gendeathrow/morechickens/items/RandomEnchantedBook.class 10, com/gendeathrow/morechickens/items/RandomPotion.class 11, com/gendeathrow/morechickens/items/SolidRF.class 12, com/gendeathrow/morechickens/items/SolidXp.class ...
知道了这些,事情就比较容易了,针对第1种和第2种,只要Mod中含有对应文件,那么读取文件再解析文本就能得到Mod的信息
做成什么样?怎么做?
- 工具需要具备以下功能
- 能够分析mods目录中所有mod的依赖关系
- 给出一个Mod能够分析出它对应的依赖信息
- 方法
- 读取解析mcmid.info文件
- 读取解析JSON文件
- 读取解析.class文件
开发过程
采用Go语言开发
解析的数据结构如下:
// asm信息,通过json或class解析 type ASMInfo struct { Name string `json:"name"` Interfaces []string `json:"interfaces"` Annotations []Annotation `json:"annotations"` ById map[int]Annotation } type Annotation struct { Type TargetType `json:"type"` Name string `json:"name"` Target string `json:"target"` Id int `json:"id"` Value ValueHolder `json:"value"` Values map[string]ValueHolder `json:"values"` PValues map[string]interface{} }
// 解析mcmod.info type ModInfo struct { ModId string `json:"modid"` Name string `json:"name"` Description string `json:"description"` Version string `json:"version"` McVersion string `json:"mcversion"` Url string `json:"url"` UpdateUrl string `json:"updateUrl"` Authors []string `json:"authors"` AuthorList []string `json:"authorList"` Credits string `json:"credits"` LogoFile string `json:"logoFile"` Screenshots []string `json:"screenshots"` Dependencies []string `json:"dependencies"` }
关键函数
// 载入Mod文件 core.LoadMod // 解析Mod之间以来关系 core.ParseDependencies // 分析Mod依赖 core.Analyze
编译
go build -o mmm
最终效果
命令一览
see命令及演示
功能包括:指定mods目录,输出依赖关系表
./mmm help see show all status Usage: mmm see [flags] Flags: -a, --analyze string analyze mods' dependencies -d, --directory string specific the "mods" directory -h, --help help for see -o, --output string specific the output path -p, --print stringArray must be "jar" or "files"
dep命令及演示
功能包括:分析指定Mod,指定目录后能够分析Mod对应的依赖文件
./mmm help dep analyze mod dependencies Usage: mmm dep [flags] Flags: -d, --directory string specific the "mods" directory -f, --file string specific a mod file -h, --help help for dep
开发总结
- 实现通过一个jar包查询Mod依赖并能够根据提供的目录指出依赖对应文件的功能
- 实现指定mods目录所有mod之间依赖关系分析以及分析统计结果并输出
5. 问题解决与结论
令人遗憾的事实
机械化而且重复的劳动不是完全没必要的
Mod开发者不一定完全遵守开发规范(约定),有时候连模组加载器都束手无策
小工具并不能直接解决mod冲突/异常/报错等问题
令人兴奋的几点
- Mod开发生态非常好,主要有以下几点
Mod开发难度相对较小,如果有需要的话,你甚至能够自己动手修复BUG(接盘)
gradle相对maven确实麻烦一些,但是官方以及第三方都提供了各种maven源(文章底部分享)
- 最终移除了三个Mod就解决了问题,服务器完美运行
- 云服务器稳定工作,现在能够随时畅玩MC!
结论
小题大做了。


相关链接 / 分享
MinecraftForge 源码
MinecraftForge/MinecraftForge(github.com)
minecraft forge maven 源
2 Browse minecraft - Crystal (lss233.com)
go语言实现的jvm
wanghongfei/mini-jvm (github.com)
本文开源项目
Minecraft到底有多少种开服工具?
itzg/docker-minecraft-server(github.com):项目中定位文件形如start-deploy{开服工具名}
,就是目前常见的开服工具,其中Vanilla表示原版Minecraft。