CoreModTutor
  • 源代码仓库(求star)
  • 目录
  • 0 绪论
  • 1 简介
    • CoreMod
    • Minecraft混淆方式
  • 2 Java虚拟机
    • ClassLoader类加载器
    • ByteCode字节码
  • 3 原版 CoreMod
    • 直接修改class文件
    • JavaAgent
    • LaunchWrapper
    • ModLauncher
  • 4 FML CoreMod
    • 1.3.2-1.5.2
    • 1.6.1-1.12.2
    • 1.13.2-1.15.2
  • 5 Mixin
    • 配置
    • 引导
    • 注入
    • 修改
    • 定位
    • 融合
    • 扩展
    • 调试
  • 6 ASM
  • 附录
    • 附录A 相关工具下载
    • 附录B 常见Java字节码指令表
    • 附录C 参考资料
Powered by GitBook
On this page
  • 1.13.2-1.15.2 FML CoreMod大事记
  • Java 还是 JavaScript?
  • 加载流程
  • 制作方法
  • coremods.json
  • JavaScript

Was this helpful?

  1. 4 FML CoreMod

1.13.2-1.15.2

1.13.2-1.15.2 FML CoreMod大事记

  • 1.13.2 引入ModLauncher

    • LaunchWrapper被舍弃

    • FML 1.3.2使用至今的Mod加载机制被重写

    • Forge实现了一定程度的模块化,由CoreMods模块调度运行CoreMod

    • 修改其他class的行为需要在JavaScript脚本中受限进行,几乎无法使用ASM Core API,需要改成ASM Tree API

    • Forge和FML开始使用大量Java 8特性

    • 不再进行运行时反混淆,转而在安装时直接调用SpecialSource生成srg混淆的Minecraft核心文件

    • binpatch也同样使用了srg混淆,在安装时对Minecraft进行修改

    • 1.13.2-25.0.199 更新ModLauncher 1.1,不再使用URLClassLoader进行Mod类加载,同时会在崩溃报告的调用栈中显示类的修改来源

    • 1.13.2-25.0.216 增加ModDirTransformerDiscoverer,ModLauncher的ITransformationService也可以被加载了

Java 还是 JavaScript?

从 1.13.2-25.0.216 开始,FML会尝试加载仅实现了ITransformationService的Mod,也就是说,我们又可以使用Java进行CoreMod的编写了。 但是,在 1.14.4-28.0.17 中,cpw将自己为Forge编写的一个ITransformationService使用JavaScript进行重写,这意味着cpw对使用JavaScript编写CoreMod是有一定程度的认可的。 使用Java或JavaScript编写CoreMod各有优劣,我们可以根据实际情况选择更为合适的写法,甚至混合编写:

项目

Java

JavaScript

可追踪性

会在崩溃报告等调试信息中显示类修改来源的转换器名,需要规范命名才有可能让用户发现问题来源

会在崩溃报告等调试信息中显示类修改来源的modid与转换器名,更易于用户发现问题来源

编写难度

需要手动实现ITransformationService与ITransformer接口,以及声明ServiceLoader,代码编写较繁琐

只需要按照格式声明要修改的内容并完成修改的函数即可

代码提示

有完整代码提示

基本语法有代码提示,尚无对ASM的完整代码提示

操作风险

通过隔离ClassLoader,不会导致类提前加载

通过限制可以使用的类,不会导致类提前加载

自由程度

除了Java的安全限制外,完全自由

受限的类访问,只能使用Java基本类、ASM与ASMAPI

ASM

可以使用Tree API与Core API

只能使用Tree API

建议在使用Tree API进行简单的修改操作时,可以考虑更为便捷的JavaScript;对于不熟悉ASM需要代码提示的开发者,或是需要Core API进行复杂的修改操作时,Java可能会是更好的选择。 若是想使用Java进行CoreMod编写,请参考ModLauncher教程;欲使用JavaScript进行CoreMod编写,请参考下文。

加载流程

(对加载流程不感兴趣的话可以直接跳过这一部分,以下代码来自1.13.2 fmllauncher和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。

coremods.json

这个文件位于jar中的META-INF/coremods.json,里面的格式如下:

{
  "脚本名称": "脚本路径",
  ...
}
  • 其中,脚本路径是相对于jar文件的路径,例如example.js代表着jar文件根目录下的example.js

一个简单的实例如下:

{
  "example": "example.js"
}

JavaScript

这是1.13.2-1.14.4 CoreMod制作的核心,这个版本不再使用Java代码来修改其他class,转为受限制的JavaScript脚本进行,在这个脚本中,只需要也只能完成修改类这一个工作,这样的设计使得CoreMod无法执行这一工作外的其他操作,极大程度的避免了诸如CoreMod调用Minecraft、普通Mod等会导致错误的操作。不过,其用于解析JavaScript的Nashorn引擎在Java 11中已被弃用。

首先,在CoreMod初始化时,脚本的initializeCoreMod函数会被调用,这个函数需要返回一个JavaScript对象来声明将要修改的目标class和修改所使用的transform函数:

function initializeCoreMod() {
    return {
        '转换器名': {
            'target': {
                'type': 'CLASS',
                'name': '目标类名'
            },
            'transformer': function (cn) {
                //transform函数
            }
        },
        ...
    };
}
  • type指定了目标的类型

    • 之前的版本只有CLASS这一个选择

    • 1.14.3-27.0.16 增加FIELD与METHOD

  • transform函数会在目标类被加载时调用,传入一个于type对应的Node(ClassNode、FieldNode或MethodNode),在游戏运行时为srg混淆,我们需要返回修改完后的Node对象,否则会出现类型转换错误

一个修改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') {
                        //请在这里对ClassNode和MethodNode进行ASM操作
                    }
                });

                //返回修改后的ClassNode对象
                return cn;
            }
        }
    };
}
  • 由于JavaScript的语言特性,全等于(===)与Java的等于(==)比较类似,在比较String时全等于与equals较为类似

  • Java.type载入的类有白名单机制,目前仅限于载入ASM库中的类与Forge提供的ASMAPI

  • 如果无需使用ClassNode对象,可以直接使用METHOD作为type,减少遍历代码

Previous1.6.1-1.12.2Next5 Mixin

Last updated 5 years ago

Was this helpful?