# 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xfl03.gitbook.io/coremodtutor/3-yuan-ban-coremod/3.3.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
