- 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
的对象,LaunchClassLoader
是URLClassLoader
的子类,在创建过程中传入了当前使用的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
列表调用接口ITweaker
的acceptOptions
将启动参数传递入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接口需要实现以下方法:
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
实例如下:
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
是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
实现如下:
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"
}
- 修改
mainClass
为net.minecraft.launchwrapper.Launch
- 在
arguments
中加入--tweakClass com.example.ExampleTweaker
- FML与LiteLoader的1.6-1.12.2均支持加载Tweaker,只需要在Manifest中写入以下内容:
TweakClass: com.example.AnotherExampleTweaker
- 放入
.minecraft/mods
文件夹即可
- 一般需要自带映射表或动态映射来进行版本兼容,不能保证运行时一定存在
IClassNameTransformer
- 使用ModLoader和库文件挂载难以使用同一个Tweaker,处理参数的方式不同
- 参数中多个
tweakClass
极有可能出错,Tweaker重复处理或都不处理启动参数均会导致Minecraft无法正常启动 - 库文件挂载较为麻烦,最终仍然需要依赖ModLoader