- 1.13.2 引入ModLauncher
- LaunchWrapper被舍弃
- FML 1.3.2使用至今的Mod加载机制被重写
- Forge实现了一定程度的模块化,由CoreMods模块调度运行CoreMod
- 修改其他class的行为需要在JavaScript脚本中受限进行,几乎无法使用ASM Core API,需要改成ASM Tree API
- Forge和FML开始使用大量Java8特性
(对加载流程不感兴趣的话可以直接跳过这一部分,以下代码来自1.13.2fmllauncher和CoreMods)
这个版本Forge仍然没有给出教程和文档,我们需要通过代码分析来自行探索。
FML在1.3.2的10个大版本之后终于再一次重写了Mod加载机制,这次重写大量的使用了ServiceLoader来运用SPI(Service Provider Interfaces)以及StreamAPI简化了加载的代码。
首先是寻找并加载CoreMod:
- 在fmllauncher(以下简称FML)的FMLLoader中
onInitialLoad
方法通过ServiceLoader获得ICoreModProvider
的实例。
其中,ICoreModProvider
的实现在CoreMods模块中,被命名为CoreModProvider
ServiceLoaderStreamUtils.errorHandlingServiceLoader(ICoreModProvider.class, serviceConfigurationError -> LOGGER.fatal(CORE, "Failed to load a coremod library, expect problems", serviceConfigurationError)).forEach(coreModProviders::add);
...
coreModProvider = coreModProviders.get(0);
- 在FML的ModDiscoverer中
discoverMods
方法首先获得modFiles
final Map<ModFile.Type, List<ModFile>> modFiles = locatorList.stream()
.peek(loc -> LOGGER.debug(SCAN,"Trying locator {}", loc))
.map(IModLocator::scanMods)
.flatMap(Collection::stream)
.peek(mf -> LOGGER.debug(SCAN,"Found mod file {} of type {} with locator {}", mf.getFileName(), mf.getType(), mf.getLocator()))
.collect(Collectors.groupingBy(ModFile::getType));
- 随后,遍历
modFiles
获得的mods
来寻找有效的Mod
for (Iterator<ModFile> iterator = mods.iterator(); iterator.hasNext(); )
{
ModFile mod = iterator.next();
if (!mod.getLocator().isValid(mod) || !mod.identifyMods()) {
LOGGER.warn(SCAN, "File {} has been ignored - it is invalid", mod.getFilePath());
iterator.remove();
brokenFiles.add(mod);
}
}
- 在遍历的过程中,
ModFile
的identifyMods
会被调用,而这个方法中会寻找CoreMod
this.coreMods = ModFileParser.getCoreMods(this);
ModFileParser
的getCoreMods
是读取CoreMod的方法,会根据Mod文件中的META-INF/coremods.json
来判断一个Mod是否是CoreMod
final Path coremodsjson = modFile.getLocator().findPath(modFile, "META-INF", "coremods.json");
if (!Files.exists(coremodsjson)) {
return Collections.emptyList();
}
- 重新回到
ModDiscoverer
中,将有效的Mod加入loadingModList
后,调用其addCoreMods
方法
loadingModList.addCoreMods();
- 在FML的LoadingModList中
addCoreMods
依次调用CoreModProvider
的addCoreMod
方法
modFiles.stream().map(ModFileInfo::getFile).map(ModFile::getCoreMods).flatMap(List::stream).forEach(FMLLoader.getCoreModProvider()::addCoreMod);
addCoreMod
方法调用了CoreMods模块CoreModEngine中的loadCoreMod
,这里初始化了稍后会使用到的Nashorn脚本引擎,这个引擎随后会被用来执行CoreMod的JavaScript来进行transform。同时,通过对脚本引擎的初始化参数,使用了白名单机制限制了脚本所使用的类,只可以使用ASM相关的类
final NashornScriptEngineFactory nashornScriptEngineFactory = new NashornScriptEngineFactory();
final ScriptEngine scriptEngine = nashornScriptEngineFactory.getScriptEngine(
s -> ALLOWED_CLASSES.stream().anyMatch(s::equals)
);
加载并初始化完CoreMod后,便是进行transform:
- ModLauncher会和原版CoreMod/ModLauncher中一样调用FMLServiceProvider实现的接口
ITransformationService
的transformers
方法,这个方法返回了ITransformer
的List
:
return new ArrayList(FMLLoader.getCoreModProvider().getCoreModTransformers());
getCoreModTransformers
调用了CoreModEngine
的initializeCoreMods
,这个方法会依次为CoreMod构造ITransformer
,并返回给ModLauncher
return coreMods.stream().map(CoreMod::buildTransformers).flatMap(List::stream).collect(Collectors.toList());
- ModLauncher会在指定的类(LaunchWrapper是所有的类)被加载时调用
CoreModClassTransformer
的transform
方法,这个方法会使用Nashorn运行JavaScript脚本
result = (ClassNode) function.call(function, input);
与1.12.2相比,1.13.2的CoreMod加载流程可谓是相当复杂,这样复杂的流程却带来了CoreMod开发的简化。
不过特别需要注意的是,1.13.2与以往不同,CoreMod必须包含在一个普通的Mod中。
通过上文的分析,我们抓住了两个关键点——coremods.json
与JavaScript。
这个文件位于jar中的META-INF/coremods.json
,里面的格式如下:
{
"脚本名称": "脚本路径",
...
}
- 其中,脚本路径是相对于jar文件的路径,例如
example.js
代表着jar文件根目录下的example.js
一个简单的实例如下:
{
"example": "example.js"
}
这是1.13.2CoreMod制作的核心,这个版本不再使用Java代码来修改其他class,转为受限制的JavaScript脚本进行,在这个脚本中,只需要也只能完成修改类这一个工作,这样的设计使得CoreMod无法执行这一工作外的其他操作,极大程度的避免了诸如CoreMod调用Minecraft、普通Mod等会导致错误的操作。
首先,在CoreMod初始化时,脚本的initializeCoreMod
函数会被调用,这个函数需要返回一个JavaScript对象来声明将要修改的目标class和修改所使用的transform
函数:
function initializeCoreMod() {
return {
'转换器名': {
'target': {
'type': 'CLASS',
'name': '目标类名'
},
'transformer': function (cn) {
//transform函数
}
},
...
};
}
transform
函数会在目标类被加载时调用,传入一个ClassNode
对象cn
,在游戏运行时cn
为srg混淆,我们需要返回修改完后的ClassNode
对象
一个修改net/minecraft/client/gui/GuiPlayerTabOverlay
中func_175249_a
(srg)方法的实例:
//使用Java.type,类似import
var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI');
var Opcodes = Java.type('org.objectweb.asm.Opcodes');
function initializeCoreMod() {
return {
'PlayerTabTransformer': {
'target': {
'type': 'CLASS',
'name': 'net/minecraft/client/gui/GuiPlayerTabOverlay'
},
'transformer': function (cn) {
//遍历ClassNode下的methods
cn.methods.forEach(function (mn) {
if (mn.name === 'func_175249_a') {
//TODO: 在这里对ClassNode和MethodNode进行ASM操作
}
});
//返回修改后的ClassNode对象
return cn;
}
}
};
}
- 由于JavaScript的语言特性,全等于(===)与Java的等于(==)比较类似
- Java.type载入的类有白名单机制,目前仅限于载入ASM库中的类与Forge提供的
ASMAPI