从 MC 程序到开发用代码,再重新到程序
这里涉及两个主要内容:Java 的反编译;类的混淆和反混淆
反编译就不多讲了,这个是每门语言都有的逆向过程,这里基于 Java 字节码的特性初步取得代码
也正是因为 Java 的反编译不难,所以 MC 采取混淆的防护手段,但对于 Java 高手来说,人工破译仅仅是时间问题
源代码我都拿到了,是不是想干什么都可以了?
但 MC 可是一个不开源的商业项目啊——虽然有条件修改游戏,而且有利于社群发展,但还是得看官方态度的
为何模组的开发会有漫长的波折和各种方案?其实是多年来官方和个人开发者拉扯的结果
换句话说,MC 模组开发史看起来非常复杂,但 MC 本身是屎山,模组开发工具也是屎山。作为一个单纯的模组开发者,摸清当今主流的方案并且会用才是有价值的,造轮子的事情都是有余力了再去了解也不迟
以下的内容基于个人对模组开发原理的初步学习总结,如有错误恳请指正
旧版本Forge处理的混淆
旧版本主要是1.12及以前,反混淆以民间组织为主,代码的基本关系:
$$
notch名(全部混淆,且无固定格式)↔srg名(类内字段和方法名格式统一但不可读)↔mcp名(字段和方法名也可读)
$$
- 为什么要用三套名字,两层映射?
mcp 名源于对 notch 名的翻译,一个 notch 名可能因为版本升级而改变用法或后续翻译优化而不断产生新版本 mcp 名
而 srg 名选择将频繁更新的部分译名用 func_
field_
开头的格式表示,保留这些混淆来保持命名的稳定性,是可读性和版本兼容性的折中策略
mcp 名是民间给出的命名,当然可以打包成一个游戏程序并运行,但精力花费极大却有最差的兼容性
(还是因为Java太好反编译了才从命名上下毒)
在很长的一段时间里,带mod端都是以 srg 名运行的,这是实现 MC 原生代码和 MCP 编写的模组共生的桥梁
srg 名几乎随MC版本而发布
mcp 名则会有很多版本,而且只影响 IDE 编写代码的命名,不影响运行,因此不用和MC、srg绑定发布
最后混淆成 srg 就可以和MC一同运行(至于如何运行,看下面的 反混淆运行原理)
1.12 等版本由于比较古老,而默认配置更古老,初次构建时可能无法顺利下载原版代码和 mapping
所以需要修改 mapping 版本,例如:
minecraft {
// mappings channel: 'snapshot', version: '20171003-1.12' 这是默认配置的版本 还是2017的snapshot
mappings channel: 'stable', version: '39-1.12' // 换较新版本
}
新版本Forge处理的混淆
有人说 Mojang 官方放出了反混淆方案并没有什么用,这里根据我今天的学习解释一下
官方反混淆不是 mcp 名形式,仍然保留了一些混淆(但比旧 srg 名要好了,主要是参数名混淆)
于是从 1.17 起,随版本稳定发布的官方半混淆方案取代了原 srg 名,感觉没啥坏处(srg 本人都被招安了)
但要注意的是,官方的反混淆没有改变 mcp 名的地位
与此同时,随着官方反混淆还一同出现了一个新项目:parchment,主要是补全官方保留的那点混淆名
在 forge 开发中,可以直接使用官方的反混淆方案,但等于没有,一般是添加一个 parchment 插件
官方给了一个半混淆方案,运行倒是不错,但想用在开发环境里就是搞笑了
不过 forge 的 MDK 有直接在注释里告诉你怎么改用 parchment
这里插一句,Gradle 用的是 Groovy 语法,语法糖甜到掉牙了
// build.gradle
plugins {
// 添加
id 'org.parchmentmc.librarian.forgegradle' version '1.+'
}
// settings.gradle
pluginManagement {
repositories {
// 添加
maven { url = 'https://maven.parchmentmc.org' }
}
}
// gradle.properties(以学习用例为例) 修改
mapping_channel=parchment
mapping_version=2023.06.26-1.20.1
于是,我们的开发实际可以认为是,最终使用了 parchment 的全套方案(包含官方混淆方案+补充内容)
而 forge 和 fabric(当然fabric仍然是可以用自己的运行方案的)借用了与官方方案对等的班混淆方案来运行
所以肯定不能说官方是没用的,只能说官方促进了模组开发的发展吗?如促。我们仍需要民间组织
其他派别的混淆处理
这个我了解的不多
比如 Fabric 就一直有一套自己的方案,当然官方提供方案后 Fabric 也能用
而其他的我们接触的比较多的就是一些服务端,在新版本都和 Forge 采用了类似的方案,即在半混淆方案上转向拥抱(或部分拥抱)官方
因为我没有具体研究过服务端的运行,所以我只确定理论上通过暴力手段兼容,Forge 和 Bukkit 等服务端是可以共存的,毕竟归根结底是同一个游戏。以 1.12 为例,比较有名的共存方案如 CatServer(MC 幻想乡用的就是这个)
后面有时间可能会了解一下服务端的处理方案
反混淆运行原理
我们自己添加的内容要在运行时加载,其实是在修改代码
如果是重新编译,那变量名都随便起,技术上也是难度较低的
- 我们主要看看 runtime 修改代码(即游戏加载时将自定义的类代码加入或取代原生的类)
本质其实就是在类加载方法中,获取类的字节码并修改,再交给类加载器
在 JVM 找到类并修改,又涉及一个概念:class文件的签名,签名有约定的格式,这里有例子:Minecraft 服务端开发指北 | IzzelAliz's Blog(不过这个作者是开发框架的,我们用框架的不需要了解太深)
总之,相当于在加载 notch 名类时拦截加载器,把类按照映射 map 改名成 srg 名,再拿去加载
(就实现了基于 notch 名的游戏程序,在运行时却使用 srg 名,而模组代码也被混淆成 srg 名,这就架起了游戏和模组兼容的桥梁)
开始添加自己的逻辑
事件模型
事件模型,源于模组的目的即修改 MC 代码,通过拦截游戏运行中的一些标志,设为“事件触发”,可以对事件进行处理,比如访问事件相关的对象,或插入自定义逻辑代码,达到修改逻辑的效果
Forge 的事件类和 Fabric 的 Callback 是他们为开发者提供的现成 API。在顺利使用事件的触发和处理前,要经过一些固定流程完成事件的的注册等操作,这个就放到后面再说了
代码注入
代码注入,至少不是 Java 开发的主流方向,算是一种邪术了。结果其在运行时插入逻辑的功能使其成了模组开发的标配
当事件 API 不足以满足插入自定义逻辑的需求时,就要手动向游戏类内注入代码了,一般默认用 Mixin 技术(作为运行时修改代码的工具,所以兼容性还不错。但一旦出现不兼容甚至 crash 也是很头疼的)
注入代码的加载原理可以参考上面的“反混淆运行原理”,至少要对 class 签名有基本的认识
比如要在某个类的某个方法下的某一处添加一段自己的代码,注入工作需要做的是:找到这个类,找到对应方法,找到修改目标行的标志(比如某个方法调用的所在行),然后写一个调用自己添加的代码的逻辑
对于每条这样的注入,都需要写一大段判断条件,来搜索自己要注入的目标位置,再修改代码,再加载
于是干脆把这个流程封装起来在编译期完成,而我们只用写注解,例如以下形式:
@Mixin(修改目标类.class)
class CustomMixin{
@Inject(method = "类下方法名", at = @At(value = "INVOKE",
target = "一长串签名(方法调用者类签名+方法参数类签名+方法返回值类签名)"))
void atSomeTime(CallbackInfo ci) {
MyTask.run();
}
}
关于代码修改暂时就说这么多,看起来好像很复杂,但 IDEA 提供的 Minecraft 开发插件(特别是代码补全功能)能协助我们完成大部分工作
后续
由于我也是初学,初学者还是不要上来就抠太多技术细节
所以接下来还是跟着开源的项目和文档,进行正常的模组开发实现流程吧~