基於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...經常一不小心就調用了原有類,然後就悲劇了.