有关Mixin引导部分的说明可以参考 Introduction to Mixins The Mixin Environment。
由于Mixin是为CoreMod服务的,所以Mixin需要使用CoreMod引导。
由于LiteLoader和Fabric已经做好了引导工作,因此不需要由模组手动引导。
也可以使用-javaagent参数引导(参见MixinAgent)。
借用Mixin wiki的一张图来说明这个问题:
因为Mixin自带一个 Tweaker 需要被添加,所以需要在LaunchWrapper还在调用实现ITweaker
的类的阶段引导Mixin,也就是上图的过程①,所以需要在CoreMod入口类合适的方法中引导。
再回顾LaunchWarpper的加载流程,因为Mixin会在TweakClasses
中添加一个Tweaker,所以不能在循环tweakClassNames
时(也就是在Tweaker的构造方法中)引导Mixin,否则会抛出java.util.ConcurrentModificationException
。
所以,Mixin需要在ITweaker
中的acceptOptions
或者injectIntoClassLoader
中引导。一个基本的Mixin引导方法如下所示:
package com.example;
import java.io.File;
import java.util.List;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.LaunchClassLoader;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.mixin.Mixins;
public class ExampleTweaker implements ITweaker {
@Override public void injectIntoClassLoader(LaunchClassLoader classLoader) {
MixinBootstrap.init();
Mixins.addConfiguration("mixins.example.json"); // 添加自己的Mixin配置文件
}
@Override public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) { }
@Override public String getLaunchTarget() { return ""; }
@Override public String[] getLaunchArguments() { return new String[0]; }
}
我们知道,FML提供了一个对ITweaker
的封装接口,也就是IFMLLoadingPlugin
,但是,MixinBootstrap
不能在这里被直接调用
因为IFMLLoadingPlugin
的实现类是通过LaunchClassLoader
加载的,ITweaker
的实现类是通过sun.misc.Launcher$AppClassLoader
加载的,而Mixin引导相关的类都会通过AppClassLoader
加载(参见MixinServiceLaunchWrapperBootstrap),所以如果直接调用,会抛出如下异常:
java.lang.NoClassDefFoundError: org/spongepowered/asm/service/IMixinService
at org.spongepowered.asm.service.MixinService.initService(MixinService.java:120)
at org.spongepowered.asm.service.MixinService.getServiceInstance(MixinService.java:111)
at org.spongepowered.asm.service.MixinService.getService(MixinService.java:106)
at org.spongepowered.asm.launch.MixinBootstrap.<clinit>(MixinBootstrap.java:77)
at com.example.ExamplePlugin.injectData(ExamplePlugin.java:60)
at net.minecraftforge.fml.relauncher.CoreModManager$FMLPluginWrapper.injectIntoClassLoader(CoreModManager.java:151)
at net.minecraft.launchwrapper.Launch.launch(Launch.java:115)
at net.minecraft.launchwrapper.Launch.main(Launch.java:28)
Caused by: java.lang.ClassNotFoundException: org.spongepowered.asm.service.IMixinService
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at net.minecraft.launchwrapper.LaunchClassLoader.findClass(LaunchClassLoader.java:106)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 8 more
那就让IFMLLoadingPlugin
实现类也用AppClassLoader
加载一次。
不推荐这么做,因为这样打破了FML加载模组的规则
package com.example;
import java.util.Map;
import net.minecraft.launchwrapper.Launch;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.mixin.Mixins;
public class ExamplePlugin implements IFMLLoadingPlugin {
public static void initMixin() {
MixinBootstrap.init();
Mixins.addConfiguration("mixins.example.json");
}
@Override public void injectData(Map<String, Object> data) {
try {
ClassLoader appClassLoader = Launch.class.getClassLoader();
MethodUtils.invokeMethod(appClassLoader, true, "addURL", this.getClass().getProtectionDomain().getCodeSource().getLocation());
MethodUtils.invokeStaticMethod(appClassLoader.loadClass(this.getClass().getName()), "initMixin");
} catch (Exception e) {}
}
@Override public String[] getASMTransformerClass() { return null; }
@Override public String getModContainerClass() { return null; }
@Override public String getSetupClass() { return null; }
@Override public String getAccessTransformerClass() { return null; }
}
在FMLCoreMod中,只需要在清单文件中添加FMLCorePluginContainsFMLMod
属性就能让这个模组也能作为普通模组加载,但是在Tweaker-Mod中是不行的,按照加载流程,FML在读取到有TweakClass
属性后就不再继续读取其他的属性了(也就是只有FMLAT
属性会被读到,其他都会被忽略)。
按照ReplayMod的方法,只需要获取到CoreModManager
类中的ignoredModFiles
字段,并从中移除模组自身文件名即可。
所以,现在应该这样:
package com.example;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.util.List;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.relauncher.CoreModManager;
import org.apache.logging.log4j.LogManager;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.mixin.Mixins;
public class ExampleTweaker implements ITweaker {
@Override public void injectIntoClassLoader(LaunchClassLoader classLoader) {
MixinBootstrap.init();
Mixins.addConfiguration("mixins.example.json");
CodeSource codeSource = this.getClass().getProtectionDomain().getCodeSource();
if (codeSource != null) {
URL location = codeSource.getLocation();
try {
File file = new File(location.toURI());
if (file.isFile()) {
CoreModManager.getReparseableCoremods().remove(file.getName());
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
} else {
LogManager.getLogger().warn("No CodeSource, if this is not a development environment we might run into problems!");
LogManager.getLogger().warn(this.getClass().getProtectionDomain());
}
}
@Override public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) { }
@Override public String getLaunchTarget() { return ""; }
@Override public String[] getLaunchArguments() { return new String[0]; }
}
现在,就可以让这个模组的@Mod
注解也被识别了。