LaunchWrapper

LaunchWrapper

  • 1.6,Minecraft更换了游戏目录的结构,使得同一文件夹中支持多个版本共存。

    为了配合这一变化,启动器中提供了从alpha、beta的大部分发布版本及1.0.0-1.5.2的正式版本,而这些版本注定是不支持新启动器与新目录结构的,用于修改老版本游戏兼容性的LaunchWrapper应运而生。

  • 由于LaunchWrapper主要由Forge团队制作,其代码大量参考了FML Relauncher,LaunchWrapper也被用于FML在1.6-1.12.2的加载,从此摆脱了修改核心文件的安装方式。

  • LaunchWrapper不支持Java9及以上的版本。

加载流程

Launch类是LaunchWrapper的主类,在这个类中创建了LaunchClassLoader的对象,初始化了ITweaker(下称Tweaker)的列表:

  • Launch的构造器中,创建了LaunchClassLoader的对象,LaunchClassLoaderURLClassLoader的子类,在创建过程中传入了当前使用的URLClassLoader使用的URLs,并更改了当前线程的ClassLoader,此后的类加载均会通过LaunchClassLoader进行

    final URLClassLoader ucl = (URLClassLoader) getClass().getClassLoader();
    classLoader = new LaunchClassLoader(ucl.getURLs());
    ...
    Thread.currentThread().setContextClassLoader(classLoader);
  • 由于从Java9开始,不再默认使用URLClassLoader而是AppClassLoader,在Java9及以上的版本这一行代码会发生ClassCastException导致游戏崩溃,直到ModLauncher才修复了这一问题

    final URLClassLoader ucl = (URLClassLoader) getClass().getClassLoader();
  • launch方法中,首先从传入的参数中读取了Tweaker的类名

    final OptionSpec<String> tweakClassOption = parser.accepts("tweakClass", "Tweak class(es) to load").withRequiredArg().defaultsTo(DEFAULT_TWEAK);
    ...
    final List<String> tweakClassNames = new ArrayList<String>(options.valuesOf(tweakClassOption));
  • 经过一番排除重复的操作后,便使用反射来实例化传入的Tweaker,并加入列表

    final ITweaker tweaker = (ITweaker) Class.forName(tweakName, true, classLoader).newInstance();
    tweakers.add(tweaker);
  • 遍历tweakers列表调用接口ITweakeracceptOptions将启动参数传递入Tweaker,以及injectIntoClassLoader会传入使用的LaunchClassLoader

    for (final Iterator<ITweaker> it = tweakers.iterator(); it.hasNext(); ) {
      final ITweaker tweaker = it.next();
      LogWrapper.log(Level.INFO, "Calling tweak class %s", tweaker.getClass().getName());
      tweaker.acceptOptions(options.valuesOf(nonOption), minecraftHome, assetsDir, profileName);
      tweaker.injectIntoClassLoader(classLoader);
      allTweakers.add(tweaker);
      it.remove();
    }
  • 上面有一个非常细节的设计,遍历的过程中从tweakers中移除已遍历的Tweaker,加入allTweakers中,再配合上一个检测tweakClassNames是否已空的循环,这样可以方便的让Tweaker在上述过程中动态加入新的Tweaker,而不需要在启动参数中指定所有的Tweaker

    allTweakers.add(tweaker);
    it.remove();
    ...
    while (!tweakClassNames.isEmpty());
  • 在初始化完所有的Tweaker后,从Tweaker中重新获得启动参数,需要注意的是,这个启动参数是所有Tweaker参数的简单合并,如果有重复参数可能会导致JOpt Simple无法按照Minecraft的要求读取参数而崩溃

    for (final ITweaker tweaker : allTweakers) {
      argumentList.addAll(Arrays.asList(tweaker.getLaunchArguments()));
    }
  • 最后调用第一个Tweaker的getLaunchTarget方法获得目标启动类,并调用这个类的main方法

    final String launchTarget = primaryTweaker.getLaunchTarget();
    final Class<?> clazz = Class.forName(launchTarget, false, classLoader);
    final Method mainMethod = clazz.getMethod("main", new Class[]{String[].class});
    ...
    mainMethod.invoke(null, (Object) argumentList.toArray(new String[argumentList.size()]));

LaunchClassLoader是LaunchWrapper加载Minecraft所使用ClassLoader,在这个类中完成了类加载与transform

  • 先把目光聚焦于loadClass这个方法中,这个方法传入一个class名,需要返回Class的实例,简而言之就是一个用于加载类的方法,在这个方法中需要完成两大任务——加载类、修改类

  • 首先,classLoaderExceptions列表记录了应当从原先的ClassLoader中加载的类,transformerExceptions记录了不应该被修改的类

    for (final String exception : classLoaderExceptions) {
      if (name.startsWith(exception)) {
          return parent.loadClass(name);
      }
    }
    ...
    for (final String exception : transformerExceptions) {
      if (name.startsWith(exception)) {
          try {
              final Class<?> clazz = super.findClass(name);
              ...
              return clazz;
          } catch (ClassNotFoundException e) {
              ...
              throw e;
          }
      }
    }
  • 对于一个可以被修改的class,会调用runTransformers方法来对其进行修改,并返回Class的对象

    final byte[] transformedClass = runTransformers(untransformedName, transformedName, getClassBytes(untransformedName));
    ...
    final Class<?> clazz = defineClass(transformedName, transformedClass, 0, transformedClass.length, codeSource);
    ...
    return clazz;
  • runTransformers方法中,依次调用Transformer对类进行修改,并返回修改后的类

    for (final IClassTransformer transformer : transformers) {
      basicClass = transformer.transform(name, transformedName, basicClass);
    }
    ...
    return basicClass;

制作方法

以上我们可以得知两个关键信息——ITweaker以及IClassTransformer

ITweaker

ITweaker接口需要实现以下方法:

  • acceptOptions,接收Minecraft启动参数,args是LaunchWrapper尚未读取的参数,gameDir是游戏路径,assetsDirassets文件夹的路径,profile是版本名称

  • injectIntoClassLoader,参数classLoader为将要用于加载Minecraft的LaunchClassLoader对象,在这个方法中,可使用registerTransformer方法进行注册IClassTransformer等操作LaunchClassLoader的行为

  • getLaunchTarget,返回需要启动的主类,一般为net.minecraft.client.main.Main,以第一个Tweaker的返回值为准

  • getLaunchArguments,返回Minecraft参数,请注意需要返回Minecraft所需的所有参数

一个简单的ITweaker实例如下:

package com.example;

import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;

import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.LaunchClassLoader;

public class ExampleTweaker implements ITweaker {

    private String[] args;

    public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) {
        String[] additionArgs = {"--gameDir", gameDir.getAbsolutePath(), "--assetsDir", assetsDir.getAbsolutePath(), "--version", profile};
        List<String> fullArgs =  new ArrayList<String>();
        fullArgs.addAll(args);
        fullArgs.addAll(Arrays.asList(additionArgs));
        this.args = fullArgs.toArray(new String[fullArgs.size()]);
    }

    public void injectIntoClassLoader(LaunchClassLoader classLoader) {
        classLoader.registerTransformer("com.example.ClassTransformer");
    }

    public String getLaunchTarget() {
        return "net.minecraft.client.main.Main";
    }

    public String[] getLaunchArguments() {
        return args;
    }
}

IClassTransformer

IClassTransformer是LaunchWrapper提供的接口,需要实现以下方法:

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

需要特别注意的是:

  • name原类名和basicClass class文件,在游戏运行时为notch混淆

  • transformedName只有当IClassNameTransformer存在时才会与name不同,具体反混淆方式取决于IClassNameTransformer的实现,例如FML会将其反混淆成mcp名称

  • basicClass可能已被其他IClassTransformer修改过

  • 切记无论如何都要返回一个有效的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
    }
}

安装方法

库文件挂载

  • 复制版本json到另一个版本文件夹中,并修改对应的名称

  • libraries中加入LaunchWrapper与自己编写的Tweaker,例如:

    {
      "name": "com.example:ExampleTweaker:1.0"
    },
    {
      "name":"net.minecraft:launchwrapper:1.12"
    }
  • 修改mainClassnet.minecraft.launchwrapper.Launch

  • arguments中加入--tweakClass com.example.ExampleTweaker

使用ModLoader加载

  • FML与LiteLoader的1.6-1.12.2均支持加载Tweaker,只需要在Manifest中写入以下内容:

    TweakClass: com.example.AnotherExampleTweaker
  • 放入.minecraft/mods文件夹即可

弊端

  • 一般需要自带映射表或动态映射来进行版本兼容,不能保证运行时一定存在IClassNameTransformer

  • 使用ModLoader和库文件挂载难以使用同一个Tweaker,处理参数的方式不同

  • 参数中多个tweakClass极有可能出错,Tweaker重复处理或都不处理启动参数均会导致Minecraft无法正常启动

  • 库文件挂载较为麻烦,最终仍然需要依赖ModLoader

Last updated