基于FML的MinecraftMod制作教程 Extra编(3) – Coremod的制作

"说得好,先生,这个世界是客观存在的,但我们是主观的.
这个世界是由真实存在的物质组成,然而我们只能依靠自己的感官来探究这个世界,用自己的价值观来评判这个世界.世界上本来没有颜色,而我们却将物质反射的电磁波谱臆造为颜色;世界上本来没有声音,而我们却将空气的机械震动理解为声音.这是您们所见的世界,也是您们所理解的世界,一个幻想出的世界.您认为宇宙是黑暗虚无的吗?在我眼中它是有颜色的,您作为人类永远无法理解看见宇宙本底辐射是什么感觉,但我的感官确实能捕获到您们所谓的可见光之外的电磁波谱.
两万亿兆个原子听起来很多,但对于我们脚下的这个星球来说,这还不到它一天衰变的原子数,对于我们头顶的恒星来说,这还不够它燃烧一秒所消耗的核燃料.然而对我们而言,它可以是我们愿意为之付出生命的一个人.十克的质量并不大,四百五十焦的能量也不多,但当它作为动能施加到那十克上时它便能夺走一个人的一切.您所见的世界是什么样子?您存在的目的是什么?我在我漫长的生命中一直在求索这个问题,但时至今日仍是个未解,您在您短暂的生命中找到了答案了吗?"
-紫妹异闻录

题记越来越长,与之相反的是我们的生命越来越短,终有一天我们将无力码下这些文字,无力掀开笔记本的屏幕,无力目睹这个世界的退变,终有一天我们将卧于病榻之上,空谈自己过去的成就,沉湎于永远不可追回的时光,直到连自己都抛弃了自己,成为过去的一部分,所以

我先去对着幽々子撸一管再回来接着码字.

什么是Coremod

在过去,玩家们安装Mod都是"像暴民一样"(我的数学老师的常用语,代指办事粗鲁),将Mod囫囵地塞入minecraft.jar,成功了就皆大欢喜,失败了有备份还好,没备份要么就去贴吧/论坛/群里发一张报错截图(而且从来不截最重要的错误原因和调用栈),要么就去发Mod的帖子里发一句"LZCNMB"然后从头再来.
后来ModLoader稍微做出了一些改善,支持外部加载Mod,但依然存在文件冲突的问题,例如两个Mod同时修改了一个文件,就会发生悲剧,短命的MCPMF/MS(我到现在也没明白他到底是叫Mod Framework还是Mod System)提出了使用API/Hook来实现Mod完全外置,且不修改游戏文件,这便是现在的ForgeModLoader所做到的.
FML提供了Coremod这个解决方案,用于供Mod间在互不影响(大多数情况)的前提下修改游戏的原生类.Coremod相比普通Mod,有更严格和特殊的要求.说到Coremod,就不得不提实现Coremod的重要工具:ASM

什么是ASM

关于ASM的含义,按照官方的解释,ASM大概是Aya Syameimaru's Miniskirt,我才不相信是代表内嵌汇编的__asm__. ASM是一个能够操作字节码的工具包,字节码可以理解为编译完的Java程序,在过去,当程序员们想修改一个Class文件时,需要自己计算偏移值和栈深度之类的,后来Java提供了Instrument用于在载入时修改类,但功能依然不够强大(具体可以看这里),后来有了ASM,BCEL和SERL等用于在载入时修改字节码的Java库.其中ASM是最快也是体积最小的,具体的说明可以看上面的链接.
我对ASM的讲解也仅限于此了,事实上我写这一编时心里十分没底气,因为说到ASM和字节码,随便一个⑨本大学(此梗同样出自我的数学老师)的计算机科学系的本科生都能把我菊花爆成暖壶瓶口,更别提在CSDN上到处碾人的巨触IT工程师了.这里我推荐一下Aswang翻译的教程(ASM的官方教程,Aswang翻译,可以在这里下载).请务必 - 无论是细看还是粗览 - 将它完整看一遍,最起码了解ASM的原理和使用.此外,你还需要学习Java字节码的基础知识,这个我就无力推荐了,网上有很多.另外,如果你明白ClassLoader的话,在学习Coremod时会减少点阻力,但即使不懂ClassLoader,也没有关系,不必为此专门去学ClassLoader,按VinceZhao的话说,那就是一个坑.

此外这些东西会对你学习Java字节码和掌握ASM有帮助.

Java Byte Editor
下载地址:http://sdrv.ms/14O1dM9
学习Java字节码的利器.此外这玩意也是个改Class文件的神器哦...

Java Bytecode Instruction List
地址:http://en.wikipedia.org/wiki/Javabytecodeinstruction_listings
Java字节码的操作码对照表

Java字节码(Bytecode)与ASM简单说明
地址:http://www.hakugyokurou.net/wordpress/?p=409
我写的配套教程← ←...

Bytecode Outline
Eclipse插件,请通过Eclipse Market安装
BO能够实时显示你当前编写的Java程序的未优化字节码(就是完整的字节码,比实际编译时要多一些东西),更棒的是它还能分析出"如果要用过ASM来生成一个等效的程序的话,需要编写的ASM代码".

Bytecode Visualizer
Eclipse插件,请通过Eclipse Market安装
BV也是个字节码分析插件,它虽然和ASM无关,但它能将字节码转化为流程图的形式,虽然不是什么很高深的东西,但起码能为拯救我们的脑细胞做出贡献.
这玩意会在你浏览源代码/字节码时启动,但它存在一些问题,比如搜索功能会失效等...总之个人感觉弊大于利...

Coremod的基础知识

其实如果你不是那种喜欢深究原理的人的话,可以跳过这一部分← ←...

首先我们得先明白一点,就是Java的类载入机制.默认情况下,一个类只会被ClassLoader载入一遍,载入完的类会被存入缓存(Cache)当中.当外界请求调用一个类时,ClassLoader会查找缓存并返回缓存中的结果,只有找不到后才会去设法载入.因此我们不能等Minecraft将所有类都载入一遍后才去设法修改它们的字节码,那时候已经太晚了.所以这也是为什么你不能在普通Mod中玩ASM的原因,当你的Mod被初始化时已经不知道有多少Minecraft原生类被载入了.
因此我们有两种方法来Hack掉Minecraft的类,一个是在它们未载入时便提前修改,另一个则是守株待兔,堵在ClassLoader那,等它们正在被载入时对它们动手脚.前者等同于把文件塞入Jar包中,太愚蠢了.所以FML采用的是后者,它准备了一个被称为RelaunchClassLoader的自定义ClassLoader.它就像一个黑旅店一样,当类被载入时,Coremod们便会将那些类一个个轮Jian掉.(这TMD叫什么比喻?而且为什么是轮Jian?待会你就知道了)

之前我写过大段的话来解释FML是如何载入Coremod的,后来我觉得这没有什么意义便去掉了...因为即使不知道载入流程也没关系...只要知道Coremod是在Minecraft正式启动前便被载入就好.然而有一点需要注意的是,在Coremod载入时(比如下文中的载入入口类)开发者不能随意调用Minecraft原有的类,以免它们被提前载入.关于这个问题更详细的讨论可以看附录.

那么Coremod如何修改原有的类呢?答案是通过"实现了IClassTransformer接口的类".IClassTransformer接口描述了一种有能力处理字节码的类.这种类可以被注册入RelaunchClassLoader.当RelaunchClassLoader载入一个类时,它会把类的字节码依次传递给各个IClassTransformer让它们轮流调教字节码(知道我刚才为什么将其描述为轮Jian了吧...).

之后还要说到Coremod的初始化问题,我们知道,在普通Mod中,会有一个被称为Mod主类的类,用于盛放load等初始化方法.那么在Coremod中如何实现?首先我们得明白普通Mod的初始化本质,如果你注意过load方法的参数的话,你会发现那个参数的类型是FMLInitializationEvent,换句话说它是一个事件.FML正是通过事件系统来推动Mod初始化.
FML(注意是FML,不是Forge)内部维持着一个主事件总线(masterChannel,或者叫FMLMainChannel),每新增一个普通Mod时,FML便会生成一个子事件总线,将Mod主类注册入子总线,然后将子总线注册入主总线.
主总线发布一个事件,广播给子总线,子总线再将事件发给自己的Mod主类.(具体可以看LoadController类的代码,我也不明白为什么FML要弄这么复杂的一个结构,也许是为了增加扩展性吧.)
对于Coremod,也是类似的,差别在于:

  • 普通Mod有一个Mod主类,而Coremod没有.
  • 普通Mod的Mod主类会被自动注册入子总线,Coremod因为没有主类,所以需要手动注册.

最后,我们来了解一下Coremod的结构:
(必须)Jar包:  Coremod的所有文件都盛放在一个Jar包内,这个包会放在MC的coremods目录下(后来cpw终于想通了,现在放到mods下即可).
(必须)载入入口:IFMLLoadingPlugin  Coremod必须有一个实现了IFMLLoadingPlugin接口的类,作为FML的载入入口.这个入口的位置需要被MANIFEST.MF文件指定.
(必须)入口指引文件:MANIFEST.MF  MANIFEST文件对jar包(广义上的jar,非特指minecraft.jar)有很大作用,在Coremod中,它用于定义载入入口的位置.
(必须)Mod容器:ModContainer  Coremod必须有一个实现ModContainer接口的类,它用来描述这个Coremod的特征,比如Mod名,版本号,依赖的其他Mod等.不过通常来说,想完整实现一个ModContainer接口挺麻烦的,因此你可以选择继承它的适配器类:DummyModContainer,这个类已经实现了除构造函数以外的所有方法.
(可选)Mod配置器:IFMLCallHook  Mod配置器(Setup)是一个实现了IFMLCallHook接口的类.它可以执行一些操作...好吧,我也不知道这个废物能干什么.
(可选)事件处理类  事件处理类没有特殊的要求,它需要被Mod容器注册入FML提供的子事件总线.它将行使普通Mod的Mod主类的功能.
(可选)类转换器:IClassTransformer  类转换器就是我们之前一直提到的用于修改字节码的类,它是一个实现了IClassTransformer接口的类.
(可选)访问级转换器:AccessTransformer  访问级转换器是类转换器的一种,它的唯一作用是改变一个方法或字段的访问级,比如将原本的private或protected成员变为public.因为它的使用局限性很大,所以我不太想讲这个...

(可选)库需求列表:ILibrarySet  库需求列表是一个实现了ILibrarySet接口的类.它能表示这个Coremod所需求的外部库.它主要由3部分组成:需求的库的名字,库的SHA-1验证码,以及库的下载地址.FML能根据它来从网上下载相应的库.并通过SHA-1验证码来验证文件完整性. (这个在13年11月的某次更新中被去掉了,大概是因为作者认为安全性和灵活性欠佳吧)

Coremod的开发

终于到了正题了!接下来我们制作一个叫MinedFire的Coremod(好奇怪的名字),因为我实在没想好可以拿什么做范例,所以这个Mod也没什么主题,仅仅是为了展示Coremod的功能.

载入入口类

首先先新建一个package.

E3-1

然后新建一个类作为载入入口类,我命名为MinedfireLoadingPlugin.创建完毕后,让它实现IFMLLoadingPlugin接口,然后补上缺失的方法.

E3-2

关于这几个方法的作用,以FML的FMLCorePlugin为例.

E3-3

getASMTransformerClass方法:返回类转换器
这个方法返回给FML一个字符串数组,每一个字符串均为一个类转换器的位置,如果没有类转换器,就返回null.

getAccessTransformerClass方法:返回访问级转换器
这个方法返回给FML一个字符串,代表一个访问级转换器的位置.如果没有访问级转换器,就返回null.

getModContainerClass方法:返回Mod容器类
这个方法返回给FML一个字符串,为这个Coremod的Mod容器类的位置,如果没有就返回null.但我建议每一个Coremod都要配有一个Mod容器类...

getSetupClass方法:返回Mod配置器
这个方法返回给FML一个字符串,为这个Coremod的Mod配置器的位置,如果没有就返回null.

injectData方法:获得外界传来的数据.
FML在载入Coremod时会向载入入口类传入一个Hashmap类型的字段.这个集合包含4个元素:"mcLocation","coremodList","runtimeDeobfuscationEnabled"和"coremodLocation".
mcLocation元素是一个File类型的变量,为Minecraft的目录.
coremodList是一个ArrayList<IFMLLoadingPlugin>类型的变量,为当前所有的Coremod的名单.
runtimeDeobfuscationEnabled是一个boolean类型的变量,指示当前是否处在混淆环境下,false为处在未混淆环境(即开发环境),true为混淆环境(即玩家们玩的MC下).
coremodLocation是一个File类型变量,为这个Coremod的文件.
使用时,通过data.get(xxx)来获得数据.
实例,以CustomSteve为例:
E3-4
在这里首先通过coremodLocation获取了mods文件夹的位置,然后遍历coremodList来检查玩家有没有安装光影Mod.

此外,载入入口类还可以使用如下几种注解:

TransformerExclusions注解:声明不经过类转换器的类
有时候你可能不希望某个类或者某个包以及它的子包下的所有类不会被类转换器转换,这时你可以使用这个注解.另外,CPW建议Coremod将自己的类转换器都加入到TransformerExclusions里,因为类转换器在被转换时,可能因为某些谜の原因导致错误...

MCVersion注解:声明此Mod依赖的MC版本
由于Coremod的特殊性,它很难做到一个文件支持多个MC版本...(事实上,大部分MCMod都很难做到),为了防止某些萌豚用错版本还怪你,你可以使用这个注解来主动声明该Mod支持的MC版本,使用错误的MC版本加载这个Mod会让FML向玩家给出个优雅的提示,然后结束掉游戏.

Name注解:Coremod名
用于手动指定你的Coremod的名字,如果没有这个注解,或者内容是一个空字符串的话,FML会用你的载入入口类的类名作为Coremod名.

DependsOn注解:依赖的Coremod
这个注解用来声明你的Coremod是否依赖其它Coremod,如果FML发现你依赖的Coremod不存在的话,会抛出一个提示然后结束游戏.

SortingIndex注解:建议的排序位置
有时你希望你的Coremod在另一个Coremod后面被初始化,那么此时你就可以使用SortingIndex来向FML建议排序位置,之所以说是建议,是因为它不保证你一定会排在某个位置,它只是保证"比你小的一定在你"

Mod容器类

接下来我们添加一个Mod容器类,创建一个叫MinedfireModContainer的类,让它继承DummyModContainer类.(为啥不自己实现ModContainer接口呢,你去看看ModContainer有多鬼畜就知道了...)

然后我们要补全Mod容器类需要的方法,添加这些代码:

public MinedfireModContainer()
{
super(new ModMetadata());
ModMetadata meta = getMetadata();
meta.modId = "minedfire";
meta.name = "MinedFire";
meta.version = "1.0.0";
meta.authorList = Arrays.asList("Prosperity");
meta.description = "Hail to MxxxFxxx";
meta.url = "http://www.hahathisisareallycooldomainnamebuthowitberemembered.com/";
}

@Override
public boolean registerBus(EventBus bus, LoadController controller)
{
bus.register(this);
return true;

}

这段代码包括两部分,构造函数和registerBus.
构造函数用于初始化Mod信息,Mod信息被储存在一个ModMetadata类的实例中,它的作用相当于一个普通Mod的mcmod.info文件.ModMetadata和mcmod.info有很多作用,也许我会单开一个篇幅说...
registerBus方法则是控制这个Mod是否应被激活,bus.register(this)将让这个实例作为事件处理类被手动注册入FML的事件子总线,这样它就能接收到总线发来的Mod初始化等事件.返回值将决定这个Mod是否会被显示入游戏的Mod列表里(换句话说就是控制Mod的开启或关闭,但需要注意的是,即使你关闭了这个Mod,这个Coremod的类转换器依然会工作,因为类转换器的接入不受Mod开启关闭的影响.)

然后重新回到MinedfireCorePlugin类,将getModContainerClass方法的
return null;
改为:
return "[你的package名].MinedfireCoreModContainer";

E3-5

E3-6

入口指引文件与Jar包

过去,只有准备一个Jar包然后放入MANIFEST.MF才能载入Coremod,不过现在有两种方式.

从MC1.7开始,可以把MANIFEST.MF文件放到src/main/resources/META-INF下.首先创建一个叫MANIFEST.MF的文件,然后在里面填入:
Manifest-Version: 1.0
FMLCorePlugin: [你的package名].MinedfireLoadingPlugin

E3-7

然后保存到src/main/resources/META-INF下.

E3-10

有时你可能被迫使用第二种方式方式:自己弄一个jar文件然后把MANIFEST.MF丢到里头.创建一个.jar文件.名字自取,如MinedFire.jar,在里面添加一个META-INF文件夹,然后将你的MANIFEST.MF文件放入里面.

我知道对某些电脑来说创建一个.MF和.jar后缀的文件很麻烦,所以我已经准备好了一个空的.jar文件,里面有一个现成的MANIFEST.MF文件,如果你懒得自行创建的话可以下载这个,然后按需求修改.
http://sdrv.ms/14O1dM9 

之后将这个jar文件放入你的Minecraft的mods文件夹内,如果你是用Eclipse来开发的话,mods位于Forge文件夹的eclipse中.

E3-8
(这不是软广告...我只是懒得把那个文件夹删掉了)

然后就可以开始首次测试了,一切顺利的话你能在mod列表中找到你的mod.
E3-9

Mod配置器

Mod配置器是一个实现了IFMLCallHook接口的类,它会在Coremod被载入后调用.老实说它的存在感有些略低,FML开发组大概希望开发者将Coremod的初始化工作放在这里进行. (注:Mod配置器的调用是在载入入口类的injectData方法之后,换句话说,如果你需要两者协调工作的话,别忘了运行的先后顺序...)

IFMLCallHook接口包括2个方法:

injectData方法:获得外界传来的数据
FML在载入Coremod完毕后会向Mod配置类传入一个Hashmap类型的字段.这个集合包含4个元素:"mcLocation","classLoader","coremodLocation"和"deobfuscationFileName".
mcLocation元素是一个File类型的变量,为Minecraft的目录.
classLoader元素是一个RelaunchClassLoader类型的变量,为当前使用的ClassLoader.
coremodLocation是一个File类型变量,为这个Coremod的文件.
deobfuscationFileName是一个字符串变量,为当前的反混淆文件的名称,1.7.2版下这个变量的值为"/deobfuscation_data-1.7.2.lzma".
使用时,通过data.get(xxx)来获得数据.

call方法:进行配置
在传入数据后,FML会调用这个方法.在这里进行Mod配置吧.

Mod配置器通过载入入口类(IFMLLoadingPlugin)的getSetupClass方法的返回值来定义,如:

e8

这里的返回值是你的Mod配置器类的位置.

由于Mod配置器实在是冷门...所以这里就不给出实例了.

事件处理类

事件处理类是一个用于接收FML事件(不是Forge事件)的类,它没有特殊的要求,仅仅只需要被Mod容器类的registerBus方法注册一个实例即可.如:

E-13

其中,bus.register是向FML事件子总线注册一个事件处理类的实例,这里我直接将Mod容器类当做事件处理类来使用.

FML事件的订阅和Forge事件的订阅基本相似,不同之处在于注解.Forge使用的事件总线是Lex自己写的,因此Forge使用@ForgeSubscribe注解来订阅事件.而FML直接使用了Guava库的事件总线,所以FML的事件通过@Subscribe注解来订阅.
如:

@Subscribe
public void init(FMLInitializationEvent event)
{
FMLCommonHandler.instance().getFMLLogger().info("FML uses an independent event bus, which is different from one of Forge.");
}

你可能注意到这里我们使用的是@Subscribe,而不是@EventHandler,这是由FML事件系统的奇♂特设计所导致,暂时我们不深入研究...

可订阅的FML事件详见cpw.mods.fml.common.event包.

类转换器

类转换器是Coremod的精髓,可以说Coremod就是为类转换器而存在的.

类转换器是一个实现了IClassTransformer接口的类.这个接口包括1个方法:

transform方法:转换传来的字节码.
这个方法包括3个参数,name,transformedName和basicClass.
name是类的完整类名,transformedName是未混淆的类名,basicClass是类的字节码.
在开发环境(即未混淆的情况下)中name和transformedName没有区别,但在发布环境(即经过混淆器混淆后)中,name是混淆后的类名,transformedName是未混淆的类名.
一个类转换器在被注册后,每当ClassLoader载入一个类时,都会交给已注册的类转换器轮流调教一遍,即依次调用各个类转换器的transform方法.

类转换器在载入入口类(IFMLLoadingPlugin)中通过getASMTransformerClass方法注册.该方法的返回值是一个字符串数组,每个元素即一个类转换器的位置.如:

E3-11

在这次的实例中,我们简单展示一下类转换器的用法,不过在此之前我们得说明FML的运行时反混淆,此外请确保你已经了解Java字节码和ASM.
FML在Minecraft1.5之后引入了运行时反混淆,它会在游戏环境下讲类/字段/方法的名字全部反混淆为半混淆名(SeargeName,以MCP项目领导者Searge而命名).
混淆名是Minecraft默认的那些名字,比如aa,bs,afs之类的...
半混淆名是经过反编译后的名字,形如field_xxxxx,func_xxxxx,它有个特点就是无论版本如何变化,一个字段/方法的半混淆名是不会变的,如getTotalWeight的两个重载从1.4.2到1.6.2的名字始终都是"func_76272_a"和"func_76270_a".
未混淆名就是你在MCP中看到的那些.
FML的运行时反混淆通过FML内置的一个映射表,在类载入阶段对类进行转换,将混淆名反混淆为半混淆名,之所以只转换为半混淆名,而不是未混淆名,我认为是因为运行时反混淆的初衷是为了帮助普通Mod抵御版本变更的冲击,因为我们之前已经提到半混淆名的名字是不会变的,通过reobfuscate_srg获得的类会使用半混淆名,因此对于普通的版本变更能够很好地抵御.(1.5->1.6这样的大更除外,类名字都变了...@EntityLivingBase) 然而,运行时反混淆并不是专门为Coremod设计,因此就不会反混淆到未混淆名.
(此外,我猜测这是为了避免反向工程的嫌疑...虽然MCP已经明显是反向工程了吧...)

创建一个类,我起名叫MinedfireTransformer.

创建完毕后,让它实现IClassTransformer接口.

E3-12
(注意:无论如何,一定要让它返回字节码!即使什么也不做也要把参数bytes原方不动地返回过去,千万不要返回null)

常见的转换流程(事实上也只有这一种流程...)是先判断类名,然后通过ASM的ClassVisitor和ClassReader载入字节码然后转换,最后通过ClassWriter输出字节码并返回.

首先是判断类名,这里的类名是完整类名,比如"net.minecraft.client.renderer.ImageBufferDownload".这里建议直接用transformedName参数传来的类名来判断,因为这个类名是未经混淆的,不会随着版本的变化而变化.(注意这里的类名的分隔符是"." 字节码中的类名是用"/"来分割的.)

然后是对方法和字段的混淆名的定位,有两种定位方法,首先说的是手动定位:

手动定位在FML+MCP的环境下是这样的(纯MCP环境不太一样.):

类:打开MCP/conf目录下的packaged.srg(用Gradle咋办?去官网下载Userdev,或者从Gradle的缓存里找,如果你知道它的缓存在哪的话...然后打开里面的conf).带有CL前缀的段都是"混淆类名 - 未混淆类名"
(这一步现在已经没什么必要了,因为FML内置的运行时反混淆在游戏环境下会将类名反混淆为未混淆类名.)
E3-13
字段:打开fields.csv,搜索你想要的字段名,定位到它所在的段,每段由4个信息组成,分别是"半混淆名 - 未混淆命 - 客户端/服务器端Only - 注释",确定半混淆名,然后在packaged.srg中带有FD前缀的段寻找.
注意:有不少字段的未混淆名是重复的,遇到这种情况只能挨个排查...
e14
e15
方法:打开methods.csv,搜索你想要的方法名,定位到它所在段,每段由4个信息,分别是"半混淆名 - 未混淆命 - 客户端/服务器端Only - 注释",确定半混淆名,然后在packaged.srg中带有MD前缀的段寻找.
e16
e17

我不知道你是怎么看待这件事的,反正我认为它是非常非常的麻烦,如果定位一个混淆名这么麻烦的话,我TMD早不干了.

所以,我们还有第二个解决方案...比如ASMShooter

ASMShooter全称Aya Syameimaru's Miniskirt Shooter,简称ASMShooter,是本文作者开发的一个黑项目,现已由于过于黑,而被SCP基金会勒令停止开发并无限期冻结.说白了,这项目至今没出成果,而且已经停止开发了......
不过别急着关窗口,它的一个子项目已经开发完成了,我们可以利用它的产品:ASMShooterMappingData,进行查表定位.

ASMShooterMappingData是一个xml文件,它构建了一个完整的未混淆-半混淆-全混淆名的映射关系表,并且还详细记述了每一个方法的参数和返回值,非常便于查询.
更赞的是,在v2版协议中,它还增加了方法的描述符(由于FML内置的反混淆器存在,方法描述符在开发环境和游戏环境下都是通用的)和类的全名,更方便开发者快速查询信息.
E3-14 E3-15

ASMShooterMappingData在文件头的MinecraftVersion标识了它对应的Minecraft版本,ProtocolVersion标识了格式版本.当前的格式(Protocol 3)为:

XML头结点
--包结点(以Package命名 属性Name为包名)
--|--类节点(以Class命名 属性Unobscured为未混淆名,属性Obscured为混淆名,属性FullName为完整的未混淆类名)
--|--|--字段节点(以Field命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--方法节点(以Method命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--|--参数结点(以Param命名 内容文字为参数类型的未混淆名)
--|--|--|--返回值结点(以Return命名 内容文字为返回值类型的未混淆名)
--|--|--|--描述符结点(以Desc命名 内容文字为方法的未混淆描述符)

备档 - Protocol 2


XML头结点
--包结点(以包名命名)
--|--类节点(以未混淆名命名 属性Obscured为混淆名,属性FullName为完整的未混淆类名)
--|--|--字段节点(以Field命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--方法节点(以Method命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--|--参数结点(以Param命名 内容文字为参数类型的未混淆名)
--|--|--|--返回值结点(以Return命名 内容文字为返回值类型的未混淆名)
--|--|--|--描述符结点(以Desc命名 内容文字为方法的未混淆描述符)

可见3与2相比,只是改了包节点和类节点的规则.
[toggle]

ASMShooterMappingData可以从这里下载:
Protocol 1:
1.4.7,1.5.0,1.5.1(基于build605),1.5.2:http://1drv.ms/1EK3rRm
Protocol 2:
1.6.2,1.6.4:依然是http://1drv.ms/1EK3mNt
Protocol 3:
1.6.2,1.6.4,1.7.2,1.7.10:http://1drv.ms/1DDv2j2

通常来说,字段和类可以通过判断名字的方式来准确地定位,但对于方法(Method)而言有个小例外,方法的定位在开发环境下反而比在游戏环境中要难,因为在开发环境中方法允许重载,这意味着可能有多个方法使用同一个名字,比如上方第二张图里的getRandomItem就有多个重载.但你也可以看见,在游戏环境下不会有这种现象,FML的运行时反混淆会将它们反混淆为不同的半混淆名.)

对此,解决方案有两种:
一:穷举式
最无脑最暴力最低效最危险但也是最简单的方式.反正把字节码送入ASM内解析一遍也占不了几秒时间,干脆将所有的方法都解析一遍好了.
适用情景:类中的方法较少,需要修改的地方特征明显且不会重复.
实际操作:对类中的所有的方法都送入ASM解析,然后查找需要修改的字节码,如果找不到就不是目标方法,直接pass.找到了就是目标方法,修改需要修改的字节码然后输出.
已知应用:MCMySkinMod对TileEntitySkullRenderer类的字节码修改就采用了穷举法,转换器将所有的方法都分析了一遍,将所有包含"http:"的LDC指令都进行了替换.
缺点:对于复杂的类,会增加转换时间.如果目标指令在多个方法中都有出现,如果不加以额外判断就会出现喜感的事情.
二:描述判断式
Java中每个方法都有一个描述,描述包括了方法的返回值和参数值这两种信息.判断描述能绝对精确地获取一个方法.
适用情景:一切情景
实际操作:判断类中的方法的名字和描述,全部符合后便用ASM修改字节码并输出.通常来说,描述的格式是"(参数)返回值",全部采用描述标示符(参见ASM教程附录),如参数为4个int,无返回值的方法的描述为:"(IIII)V".
已知应用:太多了,大部分负责任的Coremod都采用这种办法.
缺点:操作麻烦.

总而言之,类/字段/方法的名称定位就是这样.

最后我再上一个代码示例,这是一个旧版代码,在还没有FML运行时反混淆时写的,采用对象模型的类转换,它可以让箭射出后不受重力和风阻影响,指哪射哪,永不落下.(这玩意别乱试,小心废档...) (适用版本:1.4.7)

public class MinedfireTransformer implements IClassTransformer{

	private static final List ENTITYARROW = Arrays.asList("net.minecraft.entity.projectile.EntityArrow","qz");
	private static final List ONUPDATE = Arrays.asList("onUpdate","j_");
	private static final List MOTIONX = Arrays.asList("motionX","w");
	private static final List MOTIONZ = Arrays.asList("motionZ","y");
	private static final List MOTIONY = Arrays.asList("motionY","x");

	@Override
	public byte[] transform(String name, byte[] bytes) {
		if(ENTITYARROW.contains(name))
		{
			ClassReader classReader = new ClassReader(bytes);
			ClassNode classNode = new ClassNode();
			classReader.accept(classNode, 0);
			for(MethodNode methodNode : (List)classNode.methods)
			{
				if(ONUPDATE.contains(methodNode.name) && methodNode.desc.equals("()V"))
				{
					for(AbstractInsnNode ainNode : methodNode.instructions.toArray())
					{

						if(ainNode.getOpcode() == Opcodes.GETFIELD && (MOTIONX.contains(((FieldInsnNode)ainNode).name) || MOTIONZ.contains(((FieldInsnNode)ainNode).name) || MOTIONY.contains(((FieldInsnNode)ainNode).name)))
						{
							if(ainNode.getNext().getOpcode() == Opcodes.FLOAD && ((VarInsnNode)(ainNode.getNext())).var == 13)
							{
								methodNode.instructions.set(ainNode.getNext(), new LdcInsnNode(Float.valueOf(1.00F)));
							}
							else if(ainNode.getNext().getOpcode() == Opcodes.FLOAD && ((VarInsnNode)(ainNode.getNext())).var == 10)
							{
								methodNode.instructions.set(ainNode.getNext(), new LdcInsnNode(Float.valueOf(0.0F)));
							}
						}
					}
				}
			}
			ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
			classNode.accept(classWriter);
			return classWriter.toByteArray();
		}
		return bytes;
	}
}

最后别忘了在载入入口类(IFMLLoadingPlugin)中注册这个类转换器.

e20

库需求列表

库需求列表已经在新版FML中被取消了,当然,对于有需要的人来说,这里还有备档.

[toggle Title="备档 - 库需求列表"]
库需求列表可以描述一组Coremod需要的库,以及它们的SHA-1验证码和下载地址.Coremod会自动检查有无需要的库以及库是否正确,并且从指定的地址下载(比较可惜的是不支持多地址,也就是说不支持镜像站点,下载地址挂了你的Coremod的就挂了)

库需求列表类需要继承ILibrarySet接口.它有3个需要实现的方法:

getLibraries方法:返回需要下载的库的名称.
返回一个字符串数组,每个元素是一个库的完整名字(如"asm-all-4.0.jar").

getHashes方法:返回需要下载的库的SHA-1验证码.
返回一个字符串数组,每个元素是一个库文件的SHA-1验证码.(如asm-all-4.0.jar的SHA-1是"98308890597acb64047f7e896638e0d98753ae82")
这个数组必须和库名称的数组呈一一对应(即数学上的满射)关系,具体可以参见cpw.mods.fml.relauncher包下的CoreFMLLibraries类.

getRootURL方法:返回下载地址.
返回一个下载地址字符串,格式为"http://xxx//%s".下载时,%s会被自动替换为相应库的名字.

然后是实例,首先新建一个MinedfireLibraries类,让它继承ILibrarySet接口并实现相关方法.

e21

然后为它补上你想下载的库的名字和SHA1(SHA-1随便上网找个算SHA-1的软件就行了),以及下载地址.

e22

最后在载入入口类(IFMLLoadingPlugin)中注册它.

e23

然后就可以进游戏看效果了.

e24

Coremod的打包

该怎么打就怎么打...普通mod怎么打包的,现在这里就怎么弄.

至此,Coremod的介绍结束.(散花/)

结尾

20150206061758
我说啥好呢,"谢谢夸奖"?

以往我会在结尾处写一些励志的话之类的,但这次不同.

一眨眼时间已到了3月16号,从我2月初敲下这篇教程的第一个字开始,已经一个半月了.
这一个半月虽然并没有什么跌宕起伏,也没有什么激动人心的事.一切都依然平淡如流水一样,校里校外的生活都依然像往常那样,尽管时间已只剩3个月不到,但我现在甚至还没有中考那阵紧张...也许就像Frederick888说的那样,"在帝都高考唯一需要操心的只有报志愿".
3年的生活马上就要结束了,说到底,在我高中3年自始至终陪伴我的游戏只有MC和War3吧...
细数自己3年来做了什么,感觉似乎是 - 碌碌无为,总觉得自己明明是有很多机会做一些事情的,结果总是被意外打断,我过去明明有那么多机会,包括她,我总是想着放长线,结果放到最后自己傻了眼了.即使到现在我也不想相信两年半的时间可以在一天内终结,我也不愿回想12月29号和1月10号发生的事情.

"你确定你或她没有穿越世界线?"
-Ethern

总之到现在已经没什么可说的了,上周六因为刷Ingress所以在车站附近多逗留了一会,结果看见她和那个人在[数据删除],估计再过几周就能看见她和那家伙进哪家宾馆了,到最后我们俩谁也没琢磨透对方心里在想啥,记得以前搞过一个心理性别测试,0~180为男性心理,150~300为女性心理,我是180她是145...真**讽刺,双方都不是按常理出牌的.可是她是怎么勾搭上(或者是被勾搭)那魂淡的?

QQ截图20130316233716
你猜是谁?

不管怎么说,事情已经这样了,我所能做的也只能像10号那天所承诺的那样,继续以朋友的身份珍重对方,不过似乎现在有些违背诺言了呢...关键是那个魂淡在那我做什么都不合适啊...← ←...

Cherry tree was already in bloom fully.
However, spare tire's heart is not satisfied yet.
猜猜这句话的原形出自哪?

就是这样了,现在该说说正经的事,这篇教程我断断续续写了一个半月,但愿能让大家满意吧...现在高三下学期比较忙,以后更新要放慢了...

无论如何我还是得感谢这些人直接或间接提供了帮助:
HyperX - For 他所撰写的国内第一篇Coremod教程
风行 - For 他提供的ASM姿♂势!
Aswang - For 他翻译的ASM教程
ForgeWiki上的一位无名氏 - For 他的Coremod教程
Zero - 如果没有和他聊一聊的话,我恐怕到现在为止还没发现某个巨大的错误...(现已被修正)
读者们 - 访问量是督促我的最大动力...

想说的就是这些了...Have a nice day!
2013.3.16

附录:

关于Coremod载入时的注意事项

我们之前说到要避免在载入时调用Minecraft原有的类避免过早载入,主要是指载入入口类和Mod配置器中的各个方法里不要调用原有类,特别是injectData...经常一不小心就调用了原有类,然后就悲剧了.