MCP&Forge的Mod製作教程 Plus篇

"我擰.他們死.脖頸發出聲音.沒有言語,沒有尖叫,沒有咯咯聲.脖頸被折斷.折頸聲美好.折頸代表終結.折頸代表不再痛苦.折頸代表其他人的驚恐;他們更容易對付.以折頸開始,以折頸結束."
-Crunch

Plus篇是什麼?

以前寫教程時我遇到個棘手的問題,有些內容的難度是在基礎篇之上的,然而卻又夠不上Extra篇.比如對物品系統和實體系統的原理講解,顯然在基礎教程中花費大量時間講原理是不切實際的,但如果放在Extra篇又太過於坑人.所以我開了個Plus篇的坑.能夠放開了寫那些在基礎篇不敢寫,在Extra篇又不好意思寫的東西.
此外,有些東西過於糾纏,比如講Item就勢必要講ItemData,然後就自然而然要講GameData,說到GameData就不得不說MC的驗證機制,說到驗證機制就必須要講客戶端與服務器端的連接過程...這種東西無論是在基礎篇還是Extra篇中都是不方便的,只有在Plus這個瘋狂的地方才有機會說.
同時,這裡還可以放一些零七八碎的東西,一些不好分類的東西.
另外,隨着這裡的東西擴充,有些過於冗長複雜的東西可能會升級為Extra,為什麼我有一種SCP分級的感覺?

最後一點,一個東西被分到Plus不代表它不重要,不重要的東西我不會在這裡瞎扯逼...如果說基礎篇是快速速成一個新人,Extra篇是針對某方面的更深一步應用,那麼Plus篇就是對某方面的基礎原理的說明,總有人會願意深入原理的...

當前包含的內容有:
Block系統
Item系統(待完善)
GameData系統(未完)
AABB盒與Vec3

Block系統

Minecraft的Block系統以享元(Flyweight)模式儲存數據.享元模式是將大量重複的物件提取出它們共用的內容,以減少內存空間的佔用和運算力的浪費.例如:
"典型的享元模式的例子是文本處理器,它需要以圖形結構來表示字符.一個解決方案是讓每個字符都擁有其字型外觀、字模間距和其它格式信息,但這會使每個字符就耗用上千字節的內存.取而代之的解決方案是讓每個字符參照一個共享字形物件,此物件會被其它有共同特質的字符所分享;只有每個字符(文件中或頁面中)的位置才需要另外儲存." -摘自《設計模式》(╯▽╰)
再以實際需要舉例,Minecraft的一Chunk平均有20000個磚塊,大多數情況下客戶端會維持約22個Chunk處於被載入狀態.如果每一個磚塊都是一個實例,那麼你的機器中就會有44萬個類的實例,或許在大內存時代這些內存支出並不值得一提,但它們的創建和內存回收會給系統帶來巨大的壓力,大量的時間要被浪費來創建實例和回收資源.

因此,在Minecraft中你通過New Block()創建的磚塊實際上是創建一個磚塊享元,地圖數據也只儲存磚塊的ID(甚至連坐標數據也省了...),實際操作時MC會根據這些ID查找對應的享元.

Block與它的影子物品,以及GameRegister的registerBlock
在基礎篇中我們提到過物品ID和實際ID的換算問題,也提到過這是為了將磚塊的影子物品空出來.

關於影子物品,首先我們要知道玩家拿在手上的ItemStack(具體看Item的部分)只能和Item關聯,而不能和Block關聯,用便於理解(但並不符合實際)的話說,就是"能被玩家拿在手裡的是Item,而不是Block",故此當一個磚塊被創建時,遊戲還會配套創建一個影子物品,這個物品是ItemBlock類或其派生類.

那麼這個影子物品會在何時創建呢?在Block類的構造函數內找不到相關代碼,實際上,它是在GameRegister的registerBlock方法中被創建.registerBlock方法會創建一個真實ID和對應的磚塊的ID相同的物品.同時還會順便註冊這個物品(registerItem,具體定義參見Item部分和GameData部分).

registerBlock一共有5種重載.分別是:
registerBlock(Block block) 過去我們最常用的,但在GameData系統流行後被FML開發組標記為Deprecated(不推薦)了.
registerBlock(Block block, String name) 可以指定一個物品名稱,這個名稱用於預防Mod衝突,除此以外不影響遊戲...
registerBlock(Block block, Class<? extends ItemBlock> itemclass) 能指定影子物品從哪個類創建,因為不能指定名稱所以被劃為Deprecated.
registerBlock(Block block, Class<? extends ItemBlock> itemclass, String name) 既能指定影子物品從哪個類創建,又能指定名稱.
registerBlock(Block block, Class<? extends ItemBlock> itemclass, String name, String modId) 在上面的那個基礎上,還能指定Mod ID,ModID的定義參見GameData部分.
使用registerBlock註冊磚塊的本質是創建磚塊的影子物品,另外,在我的Forge(6.6.0.510)中它還負責向blockRegistry(一個以"Mod - Block"為鍵值關係儲存數據的ArrayListMultimap)中添加數據,但我實在找不到blockRegistry有在哪裡被調用過,也許這是個尚未製作完的功能吧...

Block的材質(Material)
一個Block有一個它所屬的Material(材質),Material決定了一個Block的部分物理屬性,比如是否可燃,是否可被活塞推動等(但不包括是否透光和是否發光).它是對Block的屬性的概括,比如如果開發者想創建一個能對木材質磚塊有特殊功能的物品,如果沒有Material,開發者不得不自己判斷哪個磚塊在概念上符合"木頭"的定義,而且如果有另一個Mod提供了一個木頭磚塊的話,這個開發者製作的物品就對那個新磚塊沒用了.有了Material後,開發者只需判斷受物品影響的磚塊的材質是否是木材質,即使玩家安裝了其他Mod,只要那些新磚塊的材質也是木材質,物品便依然能生效.

Metadata
在基礎篇提到過,如果要讓磚塊實現高級功能可以通過TileEntity,同樣儲存數據也可以通過TileEntity,但如果一切數據都只能通過TileEntity來儲存的話未免也太[數據刪除],故此就有了Metadata.Metadata能為一個磚塊儲存一個int32類型數據,別小看一個int32,按Indeed的話說,"是32個boolean呢".
事實上Metadata只能存儲一個4位無符號整數,也就是0~15...因為那個int32會被&15...(一下就縮水到4個boolean了,淚奔)

IBlockAccess接口
任何實現IBlockAccess接口的類都擁有操作一個磚塊的能力,目前只有World和ChunkCatch兩個類實現了IBlockAccess接口.

Item系統

老實說我還並沒有想好這一部分寫什麼...
Minecraft的Item系統同樣以享元模式儲存數據.
遊戲中,被玩家實際拿在手裡的是物品棧(ItemStack),被扔在地上的物品是EntityItem.

物品的註冊
以前我們知道物品無需註冊,創建完即可使用,那為什麼現在需要註冊呢?因為FML引入了GameData機制,用於驗證遊戲數據,這裡的註冊其實是註冊磚塊的驗證信息.即使不註冊,也是不影響遊戲的.
磚塊的註冊通過GameRegister類的registerItem方法進行.

Item類中的方法

Item類中的方法大多是供開發者重寫(Override)的,這裡列舉出一些較常用和較怪異的方法.

Minecraft原生方法(部分):

setIconIndex (參數:int par1)
設置物品的圖標在紋理集中的編號.例如紋理集第一列第一行的紋理的編號是0,第一列第二行是16,第二列第二行是17.(但願我沒說錯吧...我一直分不清行和列...)

setIconCoord (參數:int par1, int par2)
如果你懶得算編號的話,那麼這個是你的福音.你直接給出紋理的坐標位置,遊戲幫你算編號.

setMaxStackSize (參數:int par1)
設置物品的最大堆疊體積.

setHasSubtypes (參數:boolean par1)
設置是否允許擁有子類型.

setMaxDamage (參數:int par1)
設置物品最大可承受的損壞值.

setContainerItem (參數:Item par1Item)
設置空容器,比如岩漿桶(bucketLava)的空容器就是空水桶(bucketEmpty).

isDamageable ()
判斷物品是否是可損壞的,對於擁有子類型的物品來說,一定會返回false.

客戶端專有:
getIconFromDamage (參數:int par1)
獲取物品圖標,參數為損壞度,通過這個你可以做出不同損壞度下樣子不同的物品.

客戶端專有:
getIconIndex (參數:ItemStack par1ItemStack)
和上面那個一樣,只不過參數換為物品棧.

setFull3D ()
字面含義是設置此物品為全3D渲染...只有木棒和骨棒兩個物品設置了這個屬性,實際效果我也不明白,看下面這個圖吧...

前者是沒有設置setFull3D的木棒,後者是設置了setFull3D的木棒(默認為設置了setFull3D)

doesContainerItemLeaveCraftingGrid (參數:ItemStack par1ItemStack)
根據返回值判斷是否在合成後將空容器留在合成欄.

getShareTag ()
根據返回值判斷是否需要將NBT數據發送給客戶端,注意:可損壞(isDamageable返回true)的物品會被強制設為發送NBT給客戶端.

onCreated (參數:ItemStack par1ItemStack, World par2World, EntityPlayer par3EntityPlayer)
當一個物品被合成/冶煉/通過交易獲得後,取出物品欄的一瞬間引發.(僅僅只是從代碼判斷,有志者可以自行實測驗證一下)

onUpdate (參數:ItemStack par1ItemStack, World par2World, Entity par3Entity, int par4, boolean par5)
每一次遊戲循環(tick)時都會引發玩家物品欄(36個格子,包括背包欄和屏幕下側的物品條)內所有物品的onUpdate.
par4為物品在物品欄內的位置序號(具體怎麼個算法我忘了...),par5為"是否被玩家拿在手裡".

hitEntity (參數:ItemStack par1ItemStack, EntityLivingBase par2EntityLivingBase, EntityLivingBase par3EntityLivingBase)
用物品毆打實體時引發,返回值代表是否是一次有效使用.目前(MC1.4.7)僅用於成就統計(記錄有效使用次數).par2EntityLivingBase是被攻擊的實體,par3EntityLivingBase是攻擊實體的玩家(其實不一定是玩家...任何攻擊者都可以).

onBlockDestroyed (參數:ItemStack par1ItemStack, World par2World, int par3, int par4, int par5, int par6, EntityLivingBase par7EntityLivingBase)
摧毀一個磚塊時引發,返回值和hitEntity的用途相同.
par3是被摧毀的磚塊的磚塊ID.par4,5,6分別是x,y,z坐標.

canHarvestBlock (參數:Bock block)
判斷能否有效採集某個磚塊...我不敢妄下定論,因為我真沒仔細想過什麼算Harvest...也許能獲得產物的就算是Harvest?(比如鐵鎬拆鑽石礦就算canHarvest,石頭鎬就不算)

onItemUse (參數:ItemStack par1ItemStack, EntityPlayer par2EntityPlayer, World par3World, int par4, int par5, int par6, int par7, float par8, float par9, float par10)
用物品右鍵磚塊時引發,返回值和hitEntity的用途相同,都是用於統計數據.
par4,5,6分別是磚塊的x,y,z坐標.par7是磚塊的面,底面是0, 頂面是1, 東面是5, 西面是4, 北面是2, 南面是3.par8,9,10是玩家敲擊的位置的精確坐標.(Forge給的注釋是錯誤的...我以前也被坑了,誰有興趣去發個Issue?)
此外,Forge新增了一個叫onItemUseFirst的方法,屬於它的特殊版本,具體介紹在後面.

onItemRightClick (參數:ItemStack par1ItemStack, World par2World, EntityPlayer par3EntityPlayer)
玩家按下右鍵時引發.(目前尚不清楚是否與onItemUse衝突.)
若玩家持續按住右鍵,該方法可以被連續引發.請注意,玩家按住右鍵不等於使用物品(這裡對使用物品的定義是:"需要一段執行時間,有執行動作",因此用劍格擋,吃食物,拉弓都屬於使用物品,扔藥水和盛水不算,因為它們是瞬發的).若要讓玩家使用物品,請調用玩家實體的setItemInUse方法,setItemInUse方法是設置該玩家正在使用某物品.
返回值的用途我沒有研究過...

getItemUseAction (參數:ItemStack par1ItemStack)
當物品正在被使用時,玩家的動作類型.這決定了玩家的動作,以及使用完成後的結果.(比如EnumAction.eat的結果就是吃掉物品)

getMaxItemUseDuration (參數:ItemStack par1ItemStack)
物品能被持續使用的最大時間,單位是tick.
比如對於動作為EnumAction.eat的物品來說,使用超過這個時間後就算吃掉了.

onPlayerStoppedUsing (參數:ItemStack par1ItemStack, World par2World, EntityPlayer par3EntityPlayer, int par4)
當玩家停止使用物品時引發.通常來說,你不用為你之前的行為擦屁股,因為遊戲會自動清空使用狀態.但如果你之前做了什麼特別奇葩的事,不得不為你之前的行為負責的話,就在這裡點事後煙吧.(比如弓的鬆開右鍵後射出弓箭,這玩意只能自己處理...)
par4是物品被使用了多久,單位是tick.

itemInteractionForEntity (參數:ItemStack par1ItemStack, EntityLivingBase par2EntityLivingBase)
玩家對實體按下右鍵後引發.
如果你的物品在被使用後需要被消耗(比如鞍,用一次少一個)的話,別忘了在這裡減少它的數量(不必判斷是否需要銷毀,如果數量為0的話,接下來遊戲會自動銷毀它).它的返回值代表操作是否成功.

addInformation (參數:ItemStack par1ItemStack, EntityPlayer par2EntityPlayer, List par3List, boolean par4)
別被名字欺騙了,我認為MCP組擬錯了它的名字.我認為它真正的名字是getExtraInformation或getCustomInfomation,即獲取物品的額外信息.它是由ItemStack類的getTooltip(獲取物品信息欄文字)方法調用,其中par3List參數是一個元素類型為String的ArrayList集合,每一個元素代表一行文字,調用addInformation方法的真實目的是為par3List注入額外的物品信息,因此如果你堅持addInformation這個名字的話,就把它理解為"向par3List添加信息"吧.
Item類下的addInformation是沒有內容的,換句話說它只有在被重寫後才有功能.截止到MC1.4.7時,只有附魔書重寫了這個方法,用於在提示欄中給出附魔書的內容,你問工具和武器的附魔文字是怎麼弄出來的?它們採用別的辦法.
另外,par2EntityPlayer是當前的玩家,par4是判斷是否需要顯示遊戲內部信息,比如物品id,你可以理解為是否處在debug模式...

Forge新增方法(部分):

onDroppedByPlayer (參數:ItemStack item, EntityPlayer player)
當玩家扔出物品時引發,返回值為是否允許物品被扔出,true為允許.
這個方法是在onPlayerTossEvent事件發布前引發,若其返回為false,則onPlayerTossEvent也不會發布.

onItemUseFirst (參數:ItemStack stack, EntityPlayer player, World world, int x, int y, int z, int side, float hitX, float hitY, float hitZ)
onItemUse的改良版,參數和onItemUse一樣,不同的是:
它是在onItemUse之前引發,如果它返回true,則onItemUse不會引發.
它是在引發磚塊功能之前引發,對於onItemUse方法,如果玩家希望能對有功能的磚塊(比如箱子)使用右鍵的話,必須先按住Shift.OnItemUseFirst就沒有這個問題.另外如果它返回true,則磚塊功能也不會被引發.

onBlockStartBreak (參數:ItemStack itemstack, int X, int Y, int Z, EntityPlayer player)
當玩家敲碎一個磚塊時引發.
返回值為true則阻止此磚塊破裂,false則允許遊戲按正常流程繼續處理下去.
另外,在多人遊戲中,這個方法會即在客戶端引發,也會在服務器端引發,因此你可能需要過濾事件.
尚未考證:這個多人遊戲包不包括單人遊戲?畢竟現在的MC是單人遊戲=模擬多人遊戲.

onUsingItemTick (參數:ItemStack stack, EntityPlayer player, int count)
當物品處於使用狀態時,每一個tick都會引發一次這個方法.

onLeftClickEntity (參數:ItemStack stack, EntityPlayer player, Entity entity)
當玩家使用該物品左鍵一個實體(說白了就是攻擊一個實體)時引發,不同的是這個方法是在對實體造成傷害前引發.
返回值為true則阻止對實體造成傷害,因此也不會視為一次有效的攻擊,false則允許遊戲按正常流程繼續處理下去.
值得一提的是,這個方法是在AttackEntityEvent事件發布後引發.因此AttackEntityEvent事件可能會阻止該方法的引發.

getIconIndex (參數:ItemStack stack, int renderPass, EntityPlayer player, ItemStack usingItem, int useRemaining)
一個加強版的"獲得物品的圖標id".
具體用法看Item類下,此方法的官方範例.

getRenderPasses (參數:int metadata)
判斷物品需要渲染幾次,只有requiresMultipleRenderPasses方法返回值為true的話,這個方法才會起作用.只有煙花用到了這個方法,用來渲染特殊染色.

setTextureFile (參數:String texture)
設置該物品使用的紋理集~

getContainerItemStack (參數:ItemStack itemStack)
獲取這個物品的空容器的物品棧實例.

getEntityLifespan (參數:ItemStack itemStack, World world)
返回該物品被扔在地上後存留的事件,單位是tick,默認為6000(約5分鐘)

hasCustomEntity (參數:ItemStack stack)
返回該物品是否有一個自定義的EntityItem.
大部分物體在扔到地上後都是一個EntityItem,這個方法是告訴遊戲該物品是不是有一個自定義的EntityItem.

createEntity (參數:World world, Entity location, ItemStack itemstack)
如果hasCustomEntity為true的話,則遊戲會調用這個方法來獲取自定義的EntityItem.
返回值為掉落後創造的實體的實例,不一定是EntityItem的派生類.

getSmeltingExperience (參數:ItemStack item)
熔煉後獲得的經驗,範圍在0.0~1.0,返回-1則使用遊戲默認設置.
沒有研究過這個熔煉經驗是指"熔煉該物品獲得的經驗"還是"熔煉得到此物品後獲得的經驗"...

getChestGenBase (參數:ChestGenHooks chest, Random rnd, WeightedRandomChestContent original)
這個和遊戲中的箱子(獎勵箱,地牢中的箱子等...)里的隨機物品有關,chest是當前的箱子類型,rnd是隨機數生成器,original是原來的隨機物品類型(好吧我也不知道該怎麼翻譯,因為我沒寫過相關的教程)
假如我有機會寫箱子內的隨機物品的教程的話,我會說說這個...

shouldPassSneakingClickToBlock (參數:World par2World, int par4, int par5, int par6)
如果它返回true的話,那麼使用該物品右鍵磚塊不會引發磚塊功能,無論玩家是否按住了Shift.

GameData系統

GameData系統是ForgeModLoader(即FML)的一部分,它的作用是用於校驗服務器與客戶端的磚塊/物品數據是否一致,以及單人遊戲時是否存在"存檔所需的磚塊/物品缺失".(事實上僅僅只校驗物品,但由於Minecraft的特殊機制,實際上也一道校驗了磚塊)

它所要負責的有兩個,首先,它在你載入一個存檔時會讀取這個存檔的NBT數據,從中查找這個存檔使用了哪些Mod,然後驗證你是否具備那些Mod.

其次,它在連接多人服務器時會驗證雙方是否具備必須的Mod,同時還要驗證磚塊/物品數據是否一致,舉個例子,假如服務器需要使用A Mod的ID為4000的物品(也可能是磚塊),很不幸玩家沒有A Mod,但碰巧玩家裝的B Mod也有一個ID為4000的物品,在過去很可能玩家就這麼混進去了,結果十有八九會很讓人濕衫.(比如B Mod的4000是TNT(Enhanced by SCP-914),而A MOD的4000是噴氣背包...)然而GameData的存在會讓這種情況發生的概率大大降低(有多低呢?可以說,除非B Mod的作者是存心破壞,不然都能避免)

首先我們要澄清一件事情,在現在的Minecraft,單人遊戲並非是完全的單人,而是客戶端+本地服務器模擬,換句話說,驗證工作在真正的多人聯機下要做,在單人遊戲下也要做.單人驗證的是存檔的有效性,多人驗證的是物品/磚塊ID的有效性(實際上兩者的本質是相同的).然而,值得注意的是,GameData的驗證是在客戶端進行,在單人時,驗證在本機的模擬服務器端進行,多人遊戲時,服務器將數據發給客戶端,由客戶端來驗證.(2013.2.17備註:這結論是怎麼下的?也許我該再去驗證一遍)

GameData的結構
GameData的本體位於GameData類中,它是一個"靜態類",雖然沒有被聲明為抽象,但這不代表你能實例化它,它的所有方法和字段都是靜態的,這意味着你能在任何一個位置通過GameData.xxx來調用它的方法.

GameData類包含以下字段,它們全部是靜態(static),私有(private).
idMap 一個以Integer(int的包裝類型)為鍵,ItemData為值的Map,它按照"物品ID-物品的ItemData"的關係來儲存物品的ItemData數據
serverValidationLatch 一個CountDownLatch(用於完成同步操作的倒計時器,具體使用方法自行百度或查閱工具書),它作為服務器端驗證閂,當它倒數時,代表服務器端正在進行驗證,它的倒數完畢代表着服務器端完成了驗證工作.
clientValidationLatch 一個CountDownLatch,它作為客戶端驗證閂,當它倒數時,代表客戶端正在進行驗證,它的倒數完畢代表着客戶端完成了驗證工作.
difference 一個以Integer為鍵,ItemData為值的Map,它按照"物品ID-物品的ItemData"的關係來儲存客戶端與服務器端有差異的物品(即那些缺失,或者ID相同但實質不同的物品)
shouldContinue 一個bool變量,它代表服務器是否能繼續接下來的操作,如果在完成驗證後它為true,那麼就可以繼續之後的操作,如果它為false,就會拋出一個運行時異常(RuntimeException),在驗證過程中出現了重大錯誤會導致shouldContinue自動設為false.然而客戶端在完成自己的驗證工作後,可以任意設置shouldContinue的值,換句話說,即使服務器出錯了,客戶端仍能強制將shouldContinue設為true來繼續運行;即使服務器沒出錯,客戶端如果將shouldContinue設為false的話也會搞掉服務器端.(你說你想靠它搞垮一個服務器?抱歉我之前說的都是"本地模擬的服務器"...)
isSaveValid 一個bool變量,代表存檔是否完全無錯,它僅僅只供GameData內部處理用.
ignoredMods 一個以String為鍵,String為值的Map,老實說我也沒明白他的值是儲存什麼的,只知道鍵是用來儲存"忽略的Mod"的Mod ID.誰知道cpw是想幹嘛...按說用ArrayList就足夠了.

GameData類包含以下方法,它們全部是靜態(static),若沒有特殊註明則為公共(public)
isModIgnoredForIdValidation (參數:String modId)
訪問級為私有,檢查一個Mod是否是被忽略的.
newItemAdded (參數:Item item)
添加一個新物品,它會在你創建一個新物品(即Item xxx = new Item(...);時)自動調用.具體代碼詳見Item類的構造函數.
首先,這個方法會分析當前是誰在請求創建新物品,是Minecraft本身,還是一個Mod?如果是Mod,那麼是哪個Mod?
(PS:如果你在初始化階段過後創建物品的話,FML會判定你創建的物品屬於Minecraft本身,同時會向你發出警告,但通常來說不影響遊戲 - 如果你是在"單機"下...)
然後,這個方法會獲取請求者的Mod ID(Minecraft本體的ModID是"Minecraft")和磚塊的類名
然後根據這兩個信息,創建一個ItemData,並嘗試將它放入idMap,如果發生衝突則覆蓋掉舊的並擲出一個警告.
validateWorldSave (參數:Set worldSaveItems)
這個東西是要與外界聯動的,它的原理與作用我會在後面講.
writeItemData (參數:NBTTagList itemList)
將idMap中存儲的ItemData信息以NBT的形式寫入一個NBTTagList中.
initializeServerGate(參數:int gateCount)
初始化serverValidationLatch和clientValidationLatch,賦予它們的值是gateCount的值減去1,也就是如果你調用initializeServerGate(2),那麼賦予它們的值就是1.
由於CountDownLatch的特性(當CountDown到0時便會行動),調用initializeServerGate(1)其實是什麼用也沒有的...那麼這個參數到底是幹什麼用的?真相是:如果是2,那就是在單人模式下,如果是1,那就是在多人模式下.
(抽風狀態:這玩意別亂調用,它是用來線程間同步的,當服務器端校驗數據時,客戶端需要被serverValidationLatch鎖定直到服務器端校驗完成,同理,客戶端校驗時服務器端要被clientValidationLatch鎖死,線程不是鬧着玩的,HTTP_404(即YYF)說過"多進程其實比多線程要容易",我沒搞過多進程所以我信了,反正一聊起進程,C#和Java程序員都自動把頭扭過去了...)
gateWorldLoadingForValidation(無參數)
這個方法應當由客戶端(或者是Minecraft的"Main Thread"進程)調用,它的作用是獲取服務器端的校驗結果.
客戶端(再準確的說是"Main Thread"進程)會將自己鎖在serverValidationLatch上,直到服務器端完成校驗並將它放行.
之後,如果isSaveValid是true的話,就返回一個null,如果是false的話,就返回difference.
releaseGate(參數:boolean carryOn)
這個同樣只應由客戶端(同樣是那個進程...)來調用,它的作用是宣告客戶端的校驗完成並給出一個裁定結果.
它會將shouldContinue設置為carryOn的值,true代表校驗通過,false代表校驗失敗,服務器會拋出一個運行時錯誤.
你想用它在一夜間炸掉左手所有的服務器?別作死了...這玩意只供單人下的模擬服務器使用...
buildWorldItemData(參數:NBTTagList modList)
將NBTTagList還原為一組ItemData的集合(Set).
setName(參數:Item item, String name, String modId)
訪問級為默認(也就是只有同一個包下可以訪問,真是新鮮,我當時看到時差點沒反應過來...)
它的作用是為一個物品強制設定一個名字和ModID,它的訪問級是默認,這意味着你很難調用它,事實上,它是被GameRegister的registerItem調用的.
具體的作用我等一下說.

ItemData
ItemData是GameData的好基友,一個ItemData能記錄一個物品的名稱,所屬mod的ID,物品的ID,以及物品的序數(ordinal).
毫無疑問ItemData是可以同時也是必須實例化的.

ItemData類包含以下字段,他們都是私有的(private):
modOrdinals 一個以String為鍵,Multiset為值的靜態(static)Map,以"ModID-Mod的序數表"的關係存儲序數表.
modId 一個String變量,訪問級為final,這意味着你無法修改它的值.它存儲的是物品所屬的ModID.
(何為ModID?從字面意思來看,看上去是一個Mod被分配的ID.實際上,這裡的ModID是個類似命名空間(Namespace)的東西...好吧其實我也不太明白怎麼解釋(╯▽╰))
itemType 一個String變量,訪問級為final,它存儲的是物品的類的名字.
itemId 一個int變量,訪問級為final,它存儲的是物品的id.
ordinal 一個int變量,訪問級為final,它存儲的是物品的序數.我們剛才已經提到,ItemData記錄的是物品的類的名字,但是我們又知道,我們能用一個物品類創建出多個物品(Item item1 = MyItem(...); Item item2 = MyItem(...);這便有了兩個同屬一個類的物品),所以便有了序數這個東西,利用Multiset的特性(Multiset不如CountDownLatch那麼好百度,所以我簡要說明一下,它是介於Set和List之間的一個東西,事實上它根本沒有實現Set接口,它可以像List那樣添加多個重複的元素,但它會將重複的內容壓縮.舉個例子,你分別向Set,List,Multiset中添加"9","9","8"三個東西,結果在Set中是{"9","8"},在List中則是{"9","9","8"},而Multiset則是{"9"x2,"8"x1}),FML將每一個物品都往它所屬的Mod的序數表中添加一次,然後將返回值作為它的序數(Multiset的add操作的返回值是操作前該元素的個數,因此第一個物品被添加入後會獲得0這個序數.)它為使用同一個物品類的物品附上不同的序數,以此來識別它們.值得一提的是,開發者在創建物品時的順序不同會導致序數不同,因此開發者最好不要在發布穩定版本後改變物品創建順序.
forcedModId 一個String變量,儲存手動指定的Mod ID.開發者可以手動指定一個Mod ID,具體的東西我等一下說.
forcedName 一個String變量,儲存手動指定的物品名.

ItemData類包含以下方法,它們的訪問級都是公共(public):
ItemData(參數:Item item, ModContainer mc)
構造函數,首先它先判斷item是不是一個磚塊對應的"影子物品",如果是,則設置itemType為那個磚塊的類的名稱,如果不是,則設置itemType為物品的類的名稱.
之後它會讀取申請創建物品的mod的modId,然後根據id查找對應的序數表並賦予序數(ordinal).
ItemData(參數:NBTTagCompound tag)
構造函數,由一個NBTTagCompound還原出一個ItemData.
getItemType(無參數)
獲得它的物品名,如果被指定了forcedName則返回forcedName,沒有則返回itemType.
getModId(無參數)
獲得它所屬的Mod的Mod ID.如果制定了forcedModId則返回forcedModId,沒有則返回modId.
getOrdinal(無參數)
返回物品的序數.
getItemId(無參數)
返回物品的物品ID.
toNBT(無參數)
將本ItemData的內容寫入一個NBTTagCompound並返回.
hashCode(無參數)
重寫Object的hashCode方法,hashCode方法用於獲得一個類的實例的hash值(簡單而不準確的理解:一個理論上獨一無二的數字),它根據itemId和ordinal來算出一個hash值並返回.
equals(參數:Object obj)
重寫Object的equals方法,equals方法用於判斷兩個實例在邏輯上是否相等.
只有在滿足如下所有條件,才會返回true
·getModId方法的返回值相同
·getItemType方法的返回值相同
·物品ID(itemId)相同
·(isOveridden方法返回true)或者(序數(ordinal)相同)
如果你試圖拿一個不是ItemData的東西來比較,那麼這個方法會拋出一個ClassCastException異常...
toString(無參數)
重寫Object的toString方法,toString方法將實例的信息轉化為可讀的字符串並返回.(這只是名義上的要求,實際上99%的類沒有做到...因為它們不想也不需要讓使用者toString)
格式是"Item <物品ID>, Type <物品類名>, owned by <所屬Mod的Mod ID>, ordinal <序數>, name <強制指定的名字>, claimedModId <強制指定的Mod ID>".
mayDifferByOrdinal(參數:ItemData rightValue)
如果兩個物品不相等,判斷是否僅僅是序數不等.它的判斷條件是以下兩者同時滿足:
·getModId方法的返回值相同
·getItemType方法的返回值相同
它會在驗證時發現兩個物品不相等時作為補救來調用,如果通過這個判斷,那麼就勉強放過,如果這個判斷仍未通過,就視為嚴重錯誤.
你可能仍不太明白,為什麼它不判斷物品的ID?我在這裡只能說:當調用到這個方法時,兩者的物品ID一定是相同的...這個我會在之後解釋.
(活見鬼,當初我怎麼得出這個結論的?又是個費馬定理式的科學悲劇,下次我一定不能偷懶,有思路就要寫下來)
isOveridden(無參數)
判斷ItemData是否已被手動指定物品名和所屬ModID,是則返回true,否則返回false.
setName(參數:String name, String modId)
手動指定它的物品名和所屬ModID.開發者可以手動指定一個物品名和一個Mod ID.值得一提的是,同一個ModID下不能有兩個相同名字的ItemData.這和自動分配的不同,自動分配允許同一個ModID(例如Mod ID:ic)下有兩個相同名字的物品(比如有兩個ic.item.jetpack),它們會被分配不同序數以便區分,然而如果你將它們setName的話,是不允許重複的(setName本身就是為了規避風險,你不能引入新風險)

GameData的setName與ItemData的setName
通常來講,我們不鼓勵直接直接調用ItemData的setName,畢竟你得手動找出那個物品的ItemData的實例...
至於GameData的setName,它已經被GameRegistry類的registerItem方法包裝過一遍了...

單人遊戲下的驗證過程
懶得碼字了,上圖...

多人遊戲下的驗證過程
這個我還沒研究過...因為涉及到網絡問題~~(╯﹏╰)b 以後有空我再研究研究吧...

validateWorldSave的原理與作用
既然你已經看了單人遊戲下的驗證過程圖,那麼這裡你就好理解了.
validateWorldSave由服務器端調用,首先它會判斷是否有存檔文件,如果沒有那麼代表現在是在創建一個新檔,會直接視為服務器端驗證通過.
之後GameData會將當前存檔內所有的物品都匯總一遍,然後和客戶端傳來的物品數據對比,檢查有沒有差異.判斷依據是ItemData的equals標準.
如果沒有差異,則視為驗證通過.
有差異,則返回差異.
對客戶端而言,單人遊戲時可以由玩家選擇是否繼續載入.
多人遊戲時,如果是序號差異(依ItemData的mayDifferByOrdinal標準)那麼載入繼續,僅僅給出一個警告.
但如果不只是序號差異,則連接強制終止.
在返回差異或驗證通過後,服務器會等待客戶端的回應(多人遊戲時有差異則服務器直接咔嚓用戶).
最後根據回應結果,決定是否繼續啟動.

AABB盒與Vec3

AABB全稱AxisAlignedBoundingBox,即軸排列包圍盒,常被簡寫作AxisAlignedBB或AABB盒.它是一個長方體,且任意一條邊都平行於遊戲坐標系中的某一條坐標軸,這決定了它最少只需要兩個點便可繪製出來.(為什麼不叫BoundingBoxWithAxisAlignment?因為它的縮寫BBA觸犯了大忌)
這兩個點我稱之為"最小點"和"最大點",這是由它們的性質而起名的,最小點的任意一個坐標都小於最大點的對應坐標.利用長方體的軸對稱特性,其它6個點的坐標都可以被計算出來.
事實上,如果你嘗試在坐標系(X向右為正,Z向上為正,Y向屏幕外為正)中繪製它的話,你會發現最小點其實就是長方體左下頂點(沿Y軸向屏幕內方向俯視),最大點就是長方體右上頂點,這是否意味着最小點與最大點就是任意一對中心對稱的頂點?當然不會這麼蠢...截止到1.5.1,遊戲中仍還沒有檢測機制,不會比較輸入進來的兩個坐標的大小,假設一個正確的AABB盒兩個頂點的X坐標為minX=-5,maxX=5,如果你輸入進minX=5,maxX=-5,那麼在那麼在if(minX <= 0 && maxX >= 0)的判斷中你就悲劇了...

AABB盒有很多作用,小到碰撞檢測(事實上這才是它的最大的作用...),大到實體檢測,都需要用到AABB盒,AABB盒頻繁地創建和銷毀會造成性能的損失,因此遊戲使用AABB池(AABBPool)來儲存AABB盒,由於AABB盒的特性,一個舊的盒只要改6個變量就成新的盒子了,因此頻繁創建AABB盒是無必要的.(另外,AABB池是線程安全的)

利用AABB池創建一個AABB盒的代碼是:
AxisAlignedBB.getAABBPool().getAABB(minX,minY,minZ,maxX,maxY,maxZ);
其中,minX,minY,minZ是最小點的坐標.maxX,maxY,maxZ​是最大點的坐標,他們都是double類型.

AxisAlignedBB類的方法包括(若沒有特殊說明則訪問級全部為公共(public)):

getBoundingBox(參數:double minX, double minY, double minZ, double maxX, double maxY, double maxZ)​
訪問級為靜態公共,按照minX,minY,minZ,maxX,maxY,maxZ的格式創建一個AABB盒,用此方法創建的AABB盒既不是從AABB池中創建的,也不會放入AABB池.

getAABBPool(參數:無)
訪問級為靜態公共​,得到一個AABB池.

addCoord(參數:double x,double y,double z)
以此AABB盒為模板,從AABB池中創建一個新的AABB盒,並在其基礎上延伸x,y,z個長度.返回值為新的AABB盒.原AABB盒不受任何影響.
如參數是正數,就延伸最大點的坐標,若為負數,則延伸最小點的坐標.
如:若aabb1為(1,1,1,2,2,2)的AABB盒,aabb2 = aabb1.addCoord(2,0,-2)
則:aabb2為(1,1,-1,4,2,2) aabb1不變

expand(參數:double x, double y, double z)​
以此AABB盒為模板,從AABB池中創建一個新的AABB盒,並在其基礎上擴大x,y,z個長度.返回值為新的AABB盒.原AABB盒不受任何影響.​
如參數是正數,就是擴大,若為負數,就是縮小.
如:若aabb1為(1,1,1,2,2,2)的AABB盒,aabb2 = aabb1.expand(2,0,-0.25)
則:aabb2為(-1,1,1.25,4,2,1.75) aabb1不變

contract(參數:double x, double y, double z)
以此AABB盒為模板,從AABB池中創建一個新的AABB盒,並在其基礎上縮小x,y,z個長度.返回值為新的AABB盒.原AABB盒不受任何影響.
如參數是正數,就是縮小,若為負數,就是擴大.
​這玩意有啥用...contract(x,y,z)和expand(-x,-y,-z)有區別嗎.

getOffsetBoundingBox(參數:double x, double y, double z)​
以此AABB盒為模板,從AABB池中創建一個新的AABB盒,並在其基礎上偏移x,y,z個距離.返回值為新的AABB盒.原AABB盒不受任何影響.​
如參數是正數,就向該坐標軸正方向移動,若為負數,就向負方向移動.​
​如:若aabb1為(1,1,1,2,2,2)的AABB盒,aabb2 = aabb1.getOffsetBoundingBox(2,0,-2)
則:aabb2為(3,1,-1,4,2,0) aabb1不變

​calculateXOffset(參數:AxisAlignedBB par1AxisAlignedBB, double par2)​
若此AABB盒與參數AABB盒par1AxisAlignedBB在Y軸和Z軸上重疊的話,計算兩盒在X軸上的距離並返回值(double類型).
但如果par2比結果值更接近0,或者兩盒在Y軸或Z軸上無重疊的話,就返回par2.
換句話說,若兩盒在Y軸和Z軸的投影相交的話,就計算它們在X軸投影上的距離,否則返回par2.
但如果par2的絕對值小於結果的絕對值的話,就返回par2.

calculateYOffset(參數:AxisAlignedBB par1AxisAlignedBB, double par2)
calculateZOffset(參數:AxisAlignedBB par1AxisAlignedBB, double par2)
類似​calculateXOffset.​

intersectsWith(參數:AxisAlignedBB par1AxisAlignedBB)​
判斷AABB盒par1AxisAlignedBB是否與此AABB盒相交或包含.返回值為true則為相交.
判斷代碼等效於:

if(par1AxisAlignedBB.maxX > this.minX && par1AxisAlignedBB.minX < this.maxX) if(par1AxisAlignedBB.maxY > this.minY && par1AxisAlignedBB.minY < this.maxY) return (par1AxisAlignedBB.maxZ > this.minZ && par1AxisAlignedBB.minZ < this.maxZ);
return false;

因此兩盒必須為相交或包含關係.相接(兩邊重合)是不行的.

offset(參數:double x, double y, double z)​
將此AABB盒偏移x,y,z個距離.並返回此AABB盒,這個方法造成的改變會直接作用於此AABB盒上.
如:若aabb1為(1,1,1,2,2,2)的AABB盒,aabb2 = offset(2,0,-2)
則:aabb2 = aabb1 =(3,1,-1,4,2,0)

isVecInside(參數:Vec3 par1Vec3)​
判斷某個點(即Vec3,這個我們以後再說)是否在此AABB盒內.返回值為true即為在此盒內,否則返回false.
注意點在長方體的邊上會返回false.

getAverageEdgeLength(參數:無)
​獲取平均邊長,返回值為double類型.

copy(參數:無)
從AABB池中創建一個與此AABB盒一模一樣的AABB盒並返回它.

calculateIntercept(參數:Vec3 par1Vec3, Vec3 par2Vec3)​
將par1Vec3與par2Vec3兩點之間連一條直線,如果直線與此AABB盒相交的話,返回一個MovingObjectPosition,返回值的hitSide屬性為交點在AABB盒上的方向,0和1分別是下和上.2,3,4,5分別為東西北南.
hitVec屬性為交點.
如有兩個交點,則取距離par1Vec3較近的交點.

isVecInYZ(參數:Vec3 par1Vec3)
訪問級為私有(private),判斷點在YZ面的投影是否在AABB盒在YZ面的投影相交,相交返回true.
如果點在投影的邊上也會返回true.​

isVecInXZ(參數:Vec3 par1Vec3)​
isVecInXY(參數:Vec3 par1Vec3)​
類似isVecInYZ.

setBB(參數:AxisAlignedBB par1AxisAlignedBB)​
使此AABB盒成為par1AxisAlignedBB的深層副本.
好吧,用最簡單的話說,讓此AABB盒的屬性與par1AxisAlignedBB一模一樣,但this != par1AxisAlignedBB
(我才發現Mojang忘了重寫equals...這是個烏龍嗎?還是根本無必要?)
沒有返回值.

toString(參數:無)​
Mojang細心地重寫了toString,返回格式形如"box[$minX,$minY,$minZ,$maxX,$maxY,$maxZ]" (php風格,笑) 其中$minX等為具體的屬性值
比如一個(1,1,1,2,2,2)的AABB盒的toString應該是"box[1.0,1.0,1.0,2.0,2.0,2.0]".

AABB盒在遊戲中的應用主要由3處,碰撞檢測,繪製磚塊的黑框(玩家將鼠標移到磚塊上時),劃取區域.

Vec3

Vec3是Vector3D,即三維向量(或者說是矢量)的縮寫,它可以儲存3個分別代表x,y,z方向的分量長度的double變量.由於從來沒有人規定一個(x,y,z)只能表示速度而不能表示坐標,所以Vec3在Minecraft多用於表示一個點.
和AABB盒一樣,Vec3在遊戲中也會頻繁地使用,因此理論上說大量地創建和銷毀是不合算的,遊戲也提供了一個Vec3池(Vec3Pool),至少是在過去,現在Vec3池已經被廢棄了.它始終會創建新的Vec3而不是復用舊的.不太明白為什麼要這樣做,也許是為了避免多線程問題吧...

創建一個Vec3的代碼是:
Vec3.createVectorHelper(x,y,z);
x,y,z為Vec3的3個分量.

Vec3類的方法包括(若沒有特殊說明則訪問級全部為公共(public)):
(另外,由於Vec3即作向量又作點,需要注意區分用途,比如distanceTo很顯然不能計算向量間距離)

createVectorHelper(參數:double par0, double par2, double par4)​
​創建一個Vec3.

subtract(參數:Vec3 par1Vec3)​
執行向量減法,返回值為一個新向量,新向量=par1Vec3-此向量.
(!此方法只有客戶端有!)​

normalize(參數:無)
創建一個與此向量方向相同的單位向量(長度為1的向量),並作為返回值返回.
如果次向量是一個零向量的話,返回值也是一個零向量.

dotProduct(參數:Vec3 par1Vec3)​
執行向量點乘,返回值為一個新向量,新向量=par1Vec3·此向量
向量點乘即"V3=V1·V2 → (x3,y3,z3) = (x1*x2,y1*y2,z1*z2)"

crossProduct(參數:Vec3 par1Vec3)​
執行向量叉乘,返回值為一個新向量,新向量=par1Vec3x此向量
向量叉乘即"V3=V1xV2 → (x3,y3,z3) = (y1*z2-y2*z1,z1*x2-z2*x1,x1*y2-x2*y1) V3為V1與V2所成平面的一條法向量"
​(!此方法只有客戶端有!)

​addVector(參數:double x2, double y2, double z2)
創建一個新的向量並返回,新向量為(x1+x2,y1+y2,z1+z2).
為什麼這玩意客戶端和服務器都有...

distanceTo(參數:Vec3 par1Vec3)​
返回兩點間的距離.

squareDistanceTo(參數:Vec3 par1Vec3)
squareDistanceTo(參數:double par1, double par3, double par5)​
​返回兩點間的距離的平方.​

lengthVector(參數:無)
返回此向量的模長度.

​getIntermediateWithXValue​(參數:Vec3 par1Vec3, double par2)​
過此點與par1Vec3作一條線段,從線段中取x坐標為par2的一點,並返回此點.
如無法實現(如兩點x坐標一樣,則取出結果為一條線段而不是一個點;或取得的點在線段之外)則返回null

getIntermediateWithYValue(參數:Vec3 par1Vec3, double par2)
​getIntermediateWithZValue(參數:Vec3 par1Vec3, double par2)​
類似getIntermediateWithXValue.

rotateAroundX(參數:float par1)
將此向量繞X軸旋轉,par1為弧度制.多使用Math.PI作為π的常量.

rotateAroundY(參數:float par1)
​rotateAroundZ(參數:float par1) (!方法只有客戶端有!)
類似於rotateAroundX.為什麼rotateAroundZ只有客戶端有...

toString(參數:無)
toString方法的重寫,返回格式為​"(x分量,y分量,z分量)".