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分量)".