事件
总览
在 Forge 模组生态中,事件犹如脊梁骨,撑起了整个生态的运作。我们可以将事件统共分为两类:
FMLEvent
Event
FMLEvents 与模组加载有关,刻画了模组加载的方方面面:
FMLFingerprintViolationEvent
:若在模组运行时检测到签名(fingerprints)不一致,则会触发该事件。FMLInterModComms$IMCEvent
:触发于FMLInitializationEvent
与FMLPostInitializationEvent
之间。对于模组作者来说,这一事件可用于接收InterModComms
信息。FMLModDisabledEvent
:当你的模组因任何原因而被禁用时,触发该事件。FMLModIdMappingEvent
:在加入世界时,若检测到 ID 映射(ID mapping)发生了改变,则触发该事件。
FMLStateEvents(继承于 FMLEvent)重点刻画了 FML 生命周期内的不同阶段。
初始化加载阶段:
FMLConstructionEvent
:当模组构建模组与注解完毕时会触发该事件。在此时,模组列表已填充完毕,可正常访问。FMLPreInitializationEvent
:当 Forge 准备初始化模组时会触发该事件。在此事件中,你可以再次查询注解信息,查看与你模组相关的文件的位置(例如配置文件)。FMLInitializationEvent
:上一事件触发后,该事件便会触发。此时,大部分游戏内的对象已可正常访问,所以这一阶段的任务主要是构造矿物词典(OreDictionary)。FMLPostInitializationEvent
:当所有模组都经历了FMLInitializationEvent
事件后,该事件便会触发。这一阶段主要是统管整合所有模组做出的更改。FMLLoadCompleteEvent
:进入主界面后,此事件立即触发。某些模组,例如 JEI,会在此阶段中执行安排的运算。这一事件标志着整个 FML 生命周期的结束。
服务端加载阶段:
FMLServerAboutToStartEvent
:当所有的设置以及属性都初始化完毕后,触发该事件。FMLServerStartingEvent
:当世界加载完毕后该事件便会被触发。这一阶段主要是用于处理自定义指令等内容。FMLServerStartedEvent
:当服务器可以正常接受玩家登入时,触发该事件。
服务端关闭阶段:
FMLServerStoppingEvent
:当服务器关闭时,触发该事件。FMLServerStoppedEvent
:当服务器关闭的最后 1 刻运行之前触发该事件。在此之后,集成服务端会开始加载主界面。
监听事件
事件依照种类的不同,有着不同的监听以及发布方式。
只有在标注了
@Mod
注解的类中,且标注了@EventHandler
的成员方法才能正常监听到 FMLEvents。这些监听方法会以反射这种间接方式被调用。
例子
java
@Mod(modid = "modid", name = "Mod Name", version = "1.0")
public class ExampleClass {
@EventHandler
public void runOnPreInit(FMLPreInitializationEvent event) {
// 此处的代码块会加入到 FMLPreInitializationEvent 当中,随该事件触发而起效。
}
}
其余事件的监听方式则方便得多。这些监听方法会直接被调用。
通过注解:在对应的类上使用注解
@EventBusSubscriber
。- 这些类必须先被加载,这一注解才能生效。
- 如果该类同时带有
@Mod
注解,那么modid
参数并非必要。但是其他情况下还是需要填写的。 - 此类内任何监听了事件的方法,都必须为静态方法。
例子
java@EventBusSubscriber(modid = "modid") public class ExampleClass { @SubscribeEvent public static void thisIsAEventListener(Event event) { // This block of code will run when whichever Event is denoted in the argument } }
EVENT_BUS 交互:
事件依附于事件总线(Event Bus)之上。总线的存在意义是用于区分不同的事件(至少 Forge 在设计之初是抱着这一目的),但目前整体的总线设计令人相当困惑不解。
你可以在
MinecraftForge.class
中找到所有的事件总线。共有EVENT_BUS
、TERRAIN_GEN_BUS
以及ORE_GEN_BUS
三条。从技术层面上来看,模组作者可以实现独属于自己的事件总线,但是似乎鲜有人乐意干这种费力不讨好的事情。
你可以在任意的总线中调用
register
方法,再向该方法传入你想用于监听事件的类或是对象。如此你便能在该类或是对象中正常监听事件了。- 传入类的例子,注意,此时订阅事件的方法只能是静态方法!
Example
javapublic class StaticExample { public static void register() { MinecraftForge.EVENT_BUS.register(EventListener.class); } public static class EventListener { @SubscribeEvent public static void thisListenerWillRun(Event event) { // 必须为静态方法 // 参数的对应事件触发时,这里的代码块会与之一同运行。 } @SubscribeEvent public void thisListenerWillNeverRun(Event event) { // 不是静态方法则无法运行 } } }
- 传入对象的例子,注意,此时订阅事件的方法只能是对象的成员方法!
Example
javapublic class MemberExample { public static void register() { MinecraftForge.EVENT_BUS.register(new EventListener()); } public static class EventListener { @SubscribeEvent public void thisListenerWillRun(Event event) { // 注意,此时不能是静态方法 // 参数的对应事件触发时,这里的代码块会与之一同运行。 } @SubscribeEvent public static void thisListenerWillNeverRun(Event event) { // 静态方法则不能正常工作 } } }
PlayerDestroyItemEvent
PlayerDestroyItemEvent
is fired when a player destroys an item.
Hooks
PlayerControllerMP#onPlayerDestroyBlock(BlockPos)
PlayerControllerMP#processRightClick(EntityPlayer, World, EnumHand)
PlayerControllerMP#processRightClickBlock(EntityPlayerSP, WorldClient, BlockPos, EnumFacing, Vec3d, EnumHand)
EntityPlayer#attackTargetEntityWithCurrentItem(Entity)
EntityPlayer#damageShield(float)
EntityPlayer#interactOn(Entity, EnumHand)
ForgeHooks#getContainerItem(ItemStack)
PlayerInteractionManager#processRightClick(EntityPlayer, World, ItemStack, EnumHand)
PlayerInteractionManager#processRightClickBlock(EntityPlayer, World, ItemStack, EnumHand, BlockPos, EnumFacing, float, float, float)
10.PlayerInteractionManager#tryHarvestBlock(BlockPos)
Characteristics
- Event is not cancellable.
- Event does not have a result.
Behaviour
- Fired normally from ForgeEventFactory#onPlayerDestroyItem(EntityPlayer, ItemStack, EnumHand)
- Fired on MinecraftForge#EVENT_BUS
Oddities
- This event is never fired correctly for the context of the 7th hook's listed above:
ForgeHooks#getContainerItem(ItemStack)
. Forge's logic when trying to determine if the retrieved container item is destroyed is all wrong. This bug was introduced in MinecraftForge's PR#3388. Which meant that the event never fires for when the container item was actually destroyed. This was introduced when Forge was correcting all null-checks onItemStacks
toItemStack#isEmpty
calls instead. In most contexts, checkingItemStack#isEmpty
would be enough, but in this particular context, the semantics was misunderstood. AnyItemStack
that are destroyed will canonically beItemStack#isEmpty() == true && ItemStack != ItemStack.EMPTY
, meaning the logic ofif (!stack.isEmpty() && stack.isItemStackDamageable() && stack.getMetadata() > stack.getMaxDamage())
inForgeHooks#getContainerItem(ItemStack)
is indeed wrong and should have beenif (stack.isEmpty() && (stack.isItemStackDamageable() || stack.getDamage() >= stack.getMaxDamage())
. - To circumvent this oddity, one would have to make sure they handle all the logic they would have done in a
PlayerDestroyItemEvent
listener in their item's respective class'sgetContainerItem
method instead.