1.6.1-1.12.2 FML CoreMod大事记

  • 1.6 修改了游戏文件的结构,单一.minecraft文件夹内可以同时使用多个游戏版本
  • 1.6 引入LaunchWrapper
    • FML不再通过直接修改游戏的核心文件进行安装,改为先加入libraries,再通过传递参数--tweakClass使用LaunchWrapper加载FML并对Minecraft进行binpatch
    • FML开始通过LaunchWrapper提供的IClassNameTransformer进行运行时反混淆,将运行时的混淆方式从notch动态的反混淆成了srg
    • CoreMod不再通过FML提供的RelaunchClassLoader对class进行动态修改,换用LaunchWrapper对应的LaunchClassLoader
    • 取消了coremods文件夹,CoreMod可以被直接放入mods文件夹,CoreMod也可以直接包含普通Mod了
  • 1.7 引入ForgeGradle,Forge开发脱离了MCP工具
  • 1.7 MCP对Minecraft的类进行分包,不再存放于net.minecraft.src
  • 1.8 FML变更了包名,从cpw.mods.fml改成了net.minecraftforge.fml

加载流程

(对加载流程不感兴趣的话可以直接跳过这一部分,以下代码来自1.12.2FML)

Forge官方没有给出CoreMod的教程以及文档,我们需要通过探索FML如何加载CoreMod来得出制作的方法。

CoreModManager中,FML由以下方式加载CoreMod:

  • discoverCoreMods方法中,获取Mod文件列表,遍历每个Mod文件
    List<File> file_canidates = LibraryManager.gatherLegacyCanidates(mcDir);
    ...
    for (File coreMod : file_canidates)
    
  • 读取Mod文件jar中的Manifest

    jar = new JarFile(coreMod);
    mfAttributes = jar.getManifest() == null ? null : jar.getManifest().getMainAttributes();
    
  • 通过Manifest中的FMLCorePlugin属性存在与否来判断是否为CoreMod,如果不是则检测下一个Mod文件

    fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin");
    if (fmlCorePlugin == null)
    {
      // Not a coremod
      FMLLog.log.debug("Not found coremod data in {}", coreMod.getName());
      continue;
    }
    
  • 将CoreMod加入ClassPath,并进行加载

    classLoader.addURL(coreMod.toURI().toURL());
    ...
    loadCoreMod(classLoader, fmlCorePlugin, coreMod);
    
  • loadCoreMod方法中,取得IFMLLoadingPlugin实例

    Class<?> coreModClazz = Class.forName(coreModClass, true, classLoader);
    ...
    IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) coreModClazz.newInstance();
    
  • IFMLLoadingPlugin装饰为FMLPluginWrapper,加入列表

    FMLPluginWrapper wrap = new FMLPluginWrapper(coreModName, plugin, location, sortIndex, dependencies);
    loadPlugins.add(wrap);
    
  • injectCoreModTweaks方法中,FMLPluginWrapper作为ITweaker的子类直接加入LaunchWrapper的Tweaks列表中

    List<ITweaker> tweakers = (List<ITweaker>) Launch.blackboard.get("Tweaks");
    // Add the sorting tweaker first- it'll appear twice in the list
    tweakers.add(0, fmlInjectionAndSortingTweaker);
    for (FMLPluginWrapper wrapper : loadPlugins)
    {
      tweakers.add(wrapper);
    }
    

在LaunchWrapper中的ITweaker调用行为与原版 CoreMod/LaunchWrapper一致,会依据顺序逐个调用ITweaker

FMLPluginWrapper会被LaunchWrapper调用:

  • injectIntoClassLoader方法中,首先调用getASMTransformerClass获取IClassTransformer对应的class名称,使用TransformerWrapper进行装饰并向LaunchWrapper注册
    for (String transformer : coreModInstance.getASMTransformerClass())
    {
      classLoader.registerTransformer(ASMTransformerWrapper.getTransformerWrapper(classLoader, transformer, name));
    }
    

LaunchWrapper会在每个类被加载进ClassLoader之前调用IClassTransformertransform方法,通过这一方法,便可运行时动态修改其他class。

制作方法

(以下代码适用于1.8-1.12.2,1.6.2-1.7.10需要变更FML包名,1.6.2-1.6.4需要变更Minecraft包名)

通过上文对FML CoreMod加载方式的分析,我们可以得到三个关键内容——Manifest、IFMLLoadingPluginIClassTransformer,以下逐个介绍。

如果读者阅读过FML CoreMod/1.3.2-1.5.2的相关内容,会发现制作方法大同小异,无非是使用LaunchWrapper相关的类进行对应。

Manifest

Manifest有清单的意思,在这里指的是jar中的META-INF/MANIFEST.MF文件,可以通过修改build.gradle自动在打包时加入:

jar {
    manifest {
        attributes([
                "FMLCorePlugin": "com.example.ExamplePlugin",
                "FMLCorePluginContainsFMLMod": true
        ])
    }
}

其中:

  • FMLCorePlugin属性是IFMLLoadingPlugin实现类的完整类名
  • FMLCorePluginContainsFMLMod属性标记CoreMod的jar中是否还含有普通的Mod,如果为false,FML不会尝试寻找@Mod注解并加载普通Mod

读者可能会注意到,jar是会在build等task打包时才会被执行,在runClient之类的测试运行时并不会被读取到,如果需要在测试运行时使用CoreMod,还需要在build.gradle中加入jvm参数的设置:

minecraft {
    ...
    clientJvmArgs += "-Dfml.coreMods.load=com.example.ExamplePlugin"
    serverJvmArgs += "-Dfml.coreMods.load=com.example.ExamplePlugin"
}

IFMLLoadingPlugin

IFMLLoadingPlugin是FML提供的一个接口,需要实现以下几个方法:

  • getASMTransformerClass,返回IClassTransformer的完整类名构成的数组,这是CoreMod的关键
  • getModContainerClass,返回ModContainer的实现类的完整类名,可空
  • getSetupClass,返回IFMLCallhook的实现类完整类名,可空
  • injectData,可以获得mcLocationcoremodListcoremodLocation,分别是Minecraft文件夹File、CoreMod列表List、当前CoreMod文件File。这个方法与IFMLCallhook中的injectData主要区别为这个方法是在Minecraft启动后调用的,可以操作Minecraft的class
  • getAccessTransformerClass,返回一个AccessTransformer实现类完整类名,可空,一般可以直接使用FML提供的访问级转换器功能,无需自定义

同时,提供了以下几个注解来进行信息补充:

  • TransformerExclusions,指定不被CoreMod修改的包名前缀数组,例如指定了com.example后,com.example.something.Example也不会被修改
  • MCVersion,指定CoreMod适用的Minecraft版本
  • Name,指定CoreMod的名称,如果不指定,会直接使用类名作为名字
  • DependsOn,指定依赖
  • SortingIndex,指定被调用的顺序

一个简单的IFMLLoadingPlugin实现如下:

package com.example;

import java.util.Map;

import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.Name;

@Name("ExampleCoreMod")
public class ExamplePlugin implements IFMLLoadingPlugin {

    @Override
    public String[] getASMTransformerClass() {
        return new String[]{"com.example.ClassTransformer"};
    }

    @Override
    public String getModContainerClass() {
        return null;
    }

    @Override
    public String getSetupClass() {
        return null;
    }

    @Override
    public void injectData(Map<String, Object> data) {
    }

    @Override
    public String getAccessTransformerClass() {
        return null;
    }

}

IClassTransformer

IClassTransformer是LaunchWrapper提供的接口,相当于字节码修改器,需要实现以下方法:

  • transform,接收nametransformedNamebasicClass三个参数,分别是原类名、mcp无混淆类名和class文件的二进制byte数组,需要返回修改后的class文件的byte数组

需要特别注意的是:

  • name原类名和basicClass class文件,在游戏运行时为notch混淆,开发环境测试时为mcp混淆
  • basicClass可能已被其他CoreMod甚至Forge本身修改过
  • 切记无论如何都要返回一个有效的byte数组,否则会导致ClassNotFoundExceptionNoClassDefFoundError等导致的崩溃

一个没有对class进行任何修改的IClassTransformer实现如下:

package com.example;

import net.minecraft.launchwrapper.IClassTransformer;

public class ClassTransformer implements IClassTransformer {
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
        return basicClass;//特别注意需要返回basicClass
    }
}

一个对net.minecraft.client.gui.GuiPlayerTabOverlayfunc_175249_a(srg) renderPlayerlist(mcp)方法进行修改的实例:

package com.example;

import net.minecraft.launchwrapper.IClassTransformer;

import net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

public class ClassTransformer implements IClassTransformer {
    public byte[] transform(String name, String transformedName, byte[] basicClass) {
        if (!"net.minecraft.client.gui.GuiPlayerTabOverlay".equals(transformedName))
            return basicClass;

        //使用ASM读入basicClass
        ClassReader cr = new ClassReader(basicClass);
        ClassNode cn = new ClassNode();
        cr.accept(cn, 0);

        //遍历methods
        for (MethodNode mn : cn.methods) {
            //调用FML接口获得方法名,运行时获得的是srg,测试时获得的是mcp
            String methodName = FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(name, mn.name, mn.desc);
            if(!"func_175249_a".equals(methodName) && !"renderPlayerlist".equals(methodName)) 
                continue;

            //TODO: 在这里进行ASM操作
        }

        //返回修改后的bytes
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        cn.accept(cw);
        return cw.toByteArray();
    }
}

results matching ""

    No results matching ""