# LaunchWrapper

## LaunchWrapper

* 1.6，Minecraft更换了游戏目录的结构，使得同一文件夹中支持多个版本共存。 &#x20;

  为了配合这一变化，启动器中提供了从alpha、beta的大部分发布版本及1.0.0-1.5.2的正式版本，而这些版本注定是不支持新启动器与新目录结构的，用于修改老版本游戏兼容性的[LaunchWrapper](https://github.com/Mojang/LegacyLauncher)应运而生。
* 由于LaunchWrapper主要由Forge团队制作，其代码大量参考了FML Relauncher，LaunchWrapper也被用于FML在1.6-1.12.2的加载，从此摆脱了修改核心文件的安装方式。   &#x20;
* LaunchWrapper不支持Java9及以上的版本。

### 加载流程

[Launch](https://github.com/Mojang/LegacyLauncher/blob/master/src/main/java/net/minecraft/launchwrapper/Launch.java)类是LaunchWrapper的主类，在这个类中创建了`LaunchClassLoader`的对象，初始化了`ITweaker`（下称Tweaker）的列表：

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

  ```java
  final URLClassLoader ucl = (URLClassLoader) getClass().getClassLoader();
  classLoader = new LaunchClassLoader(ucl.getURLs());
  ...
  Thread.currentThread().setContextClassLoader(classLoader);
  ```
* 由于从Java9开始，不再默认使用`URLClassLoader`而是`AppClassLoader`，在Java9及以上的版本这一行代码会发生`ClassCastException`导致游戏[崩溃](https://github.com/Mojang/LegacyLauncher/pull/11)，直到ModLauncher才修复了这一问题

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

  ```java
  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，并加入列表

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

  ```java
  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

  ```java
  allTweakers.add(tweaker);
  it.remove();
  ```

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

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

  ```java
  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](https://github.com/Mojang/LegacyLauncher/blob/master/src/main/java/net/minecraft/launchwrapper/LaunchClassLoader.java)是LaunchWrapper加载Minecraft所使用`ClassLoader`，在这个类中完成了类加载与`transform`：

* 先把目光聚焦于`loadClass`这个方法中，这个方法传入一个class名，需要返回`Class`的实例，简而言之就是一个用于加载类的方法，在这个方法中需要完成两大任务——加载类、修改类
* 首先，`classLoaderExceptions`列表记录了应当从原先的`ClassLoader`中加载的类，`transformerExceptions`记录了不应该被修改的类

  ```java
  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`的对象

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

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

### 制作方法

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

#### ITweaker

[ITweaker](https://github.com/Mojang/LegacyLauncher/blob/master/src/main/java/net/minecraft/launchwrapper/ITweaker.java)接口需要实现以下方法：

* `acceptOptions`，接收Minecraft启动参数，`args`是LaunchWrapper尚未读取的参数，`gameDir`是游戏路径，`assetsDir`是`assets`文件夹的路径，`profile`是版本名称
* `injectIntoClassLoader`，参数`classLoader`为将要用于加载Minecraft的`LaunchClassLoader`对象，在这个方法中，可使用`registerTransformer`方法进行注册`IClassTransformer`等操作`LaunchClassLoader`的行为
* `getLaunchTarget`，返回需要启动的主类，一般为`net.minecraft.client.main.Main`，以第一个Tweaker的返回值为准
* `getLaunchArguments`，返回Minecraft参数，请注意需要返回Minecraft所需的所有参数

一个简单的`ITweaker`实例如下：

```java
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`，接收`name`、`transformedName`与`basicClass`三个参数，分别是原类名、反混淆类名和class文件的二进制`byte`数组，需要返回修改后的class文件的`byte`数组

需要特别注意的是：

* `name`原类名和`basicClass` class文件，在游戏运行时为notch混淆
* `transformedName`只有当`IClassNameTransformer`存在时才会与`name`不同，具体反混淆方式取决于`IClassNameTransformer`的实现，例如FML会将其反混淆成mcp名称
* `basicClass`可能已被其他`IClassTransformer`修改过
* 切记无论如何都要返回一个有效的`byte`数组，否则会导致`ClassNotFoundException`、`NoClassDefFoundError`等导致的崩溃

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

```java
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，例如：

  ```javascript
  {
    "name": "com.example:ExampleTweaker:1.0"
  },
  {
    "name":"net.minecraft:launchwrapper:1.12"
  }
  ```
* 修改`mainClass`为`net.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
