Skip to content

Latest commit

 

History

History
179 lines (128 loc) · 9.43 KB

7-2.md

File metadata and controls

179 lines (128 loc) · 9.43 KB

7-2 自定义事件

Bukkit 为我们提供了一些事件,虽然对于原版而言是够用了,但对于插件的二次开发,这还是不够用的,举个例子,在登录插件中,玩家登录成功事件,怎么监听?如果采用各自定义的 API,就会使得开发变得极其麻烦。

有鉴于此,插件开发社区达成了一个共识:对于插件内有价值的事件,采用自定义事件进行触发

自定义事件实际上和药水效果很像,都是通过「暴力地」直接继承某个 Bukkit 的对象。

创建事件类

如果你忘了事件处理的相关内容,请重新看一看 2-2。

所有的事件都继承了 Event 类,Event 类下还有 PlayerEventInventoryEvent 等抽象类和 PlayerInteractEvent 这样的具体可监听的类。

要进行自定义事件,我们需要找一个最近似的类进行继承。比如,如果你的事件与玩家有关,那就继承 PlayerEvent,更进一步,如果你的事件与玩家登录都有关,那就继承 PlayerLoginEvent

下面我们就以 HarmonyAuth SMART 中的「登录成功」事件为例,演示自定义事件的方法。

首先创建一个类用于描述这个事件,虽然这个事件叫做「登录成功」,但它和 PlayerLoginEvent 却没什么关联,因此我们不继承它,而只继承 PlayerEvent 这个抽象类。

package rarityeg.harmonyauthsmart;

import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;

import javax.annotation.Nonnull;

public class HASPlayerLoginEvent extends PlayerEvent implements Cancellable {
    // 是个可取消事件,需要实现 Cancellable
    private static final HandlerList handlers = new HandlerList();
    // handlers 是与这个类挂钩的处理器们
    private boolean isCancelled = false;
    // 是否取消标志位
    protected final boolean isAutoLogin;
    // 我们自己实现的功能——是否是自动登录

    @Override
    @Nonnull
    public HandlerList getHandlers() {
        // 事件对象的「获取处理器」方法
        return handlers;
    }

    public static HandlerList getHandlerList() {
        // 事件类的「获取处理器」方法
        return handlers;
    }

    public HASPlayerLoginEvent(Player p) {
        // 构造方法
        super(p, true);
    }

    @Override
    public boolean isCancelled() {
        // Cancellable 需要
        return isCancelled;
    }

    @Override
    public void setCancelled(boolean b) {
        // Cancellable 需要
        isCancelled = b;
    }
    public boolean isAutoLogin() {
        // 自己添加的功能
        return isAutoLogin;
    }
}

这里的 HandlerList 需要进行一些说明。

getHandlersgetHandlerList(静态)是必不可少的方法,Bukkit 利用这个表来记录这个事件类有哪些处理器。getHandlers 是成员方法,用于在事件实例上获取这张表,而 getHandlerList 是静态方法,用于从这个类本身获取这张表。

另外请记住,getHandlerList 这个静态方法的类才可以被监听

?> 到底怎么回事
当有事件发生时,Bukkit 或者插件会创建这个事件的一个实例,之后 Bukkit 看着 getHandlers 方法决定要将这个事件实例分配给哪些处理器,这就是 getHandlers 的由来。
那为什么需要 getHandlerList 这个静态方法呢?
回想一下我们监听事件时,我对事件处理函数的描述:「(事件处理函数的)函数名不重要,重要的是参数类型」。
在注册事件处理器时,事件实例还没有出现(因为事件还没有发生),但是此时事件处理器急着要注册,Bukkit 必须找个地方记录那些处理函数,Bukkit 的选择就是在事件所属的类中进行记录,对应的方法就是 getHandlerList
由于「该变量(handlers不属于哪个对象,属于整个类共有」(参见 2-1),因此所有的该类事件实例都共享这个表,那么它们自然就具有了相同的事件处理器(即,每个事件都被公平对待)。
这样就很明白啦,有 getHandlerList 方法的类,Bukkit 可以将事件处理函数写在其中,那也就是可以监听嘛,反之,如果没有这个方法,那 Bukkit 没办法记住这个类有哪些处理函数,那不就是不可监听嘛。

如果看不懂以上内容也没关系,总之,记住,要自定义事件,getHandlersgetHandlerList 是必须的,当作固定格式来写就好啦。

另外,事件的取消并不是在事件中完成的,而是由事件的分发者查看取消标志来决定后续的处理,下面就会看到。

我们还为这个事件添加了属于我们的一些功能,例如 isAutoLogin 来查看这次登录是否是自动登录。自定义自定义嘛,有额外的功能也很重要。

分发事件和取消事件

接下来我们需要在我们的代码中考虑什么时候触发这个事件。想一想,我们的插件中,哪个时机适合触发「登录成功」事件?自然是玩家切实登录成功的适合嘛。

触发事件时一定要遵循实事求是的原则。

HarmonyAuth SMART 中只有三个地方判定玩家切实登录,一个是登录命令,一个是注册命令,还有一个是自动登录。我们的修改如下:

// 登录命令节选
if (idm.getPasswordHash(id).equals(Util.calculateMD5(args[0]))) {
    // 当玩家密码正确时,登录
    HASPlayerLoginEvent event = new HASPlayerLoginEvent(player, false);
    // 创建事件实例,不是自动登录,填 false
    Bukkit.getPluginManager().callEvent(event);
    // 让 Bukkit 分发事件
    if (event.isCancelled()) {
        // 看看,如果这个事件被取消了,就不执行后面的玩家登录操作
        sti(id);
        return;
    }
    // 后略
}
// 注册命令节选
if (args.length < 2 || !args[0].equals(args[1])) {
    player.sendMessage(Util.getAndTranslate("msg.register-failed"));
    sti(id);
    return;
}
// 密码一致,注册成功
HASPlayerLoginEvent event = new HASPlayerLoginEvent(player, false);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
    sti(id);
    return;
}
// 自动登录的处理
if (seconds <= HarmonyAuthSMART.instance.getConfig().getInt("auto-login")) {
    HASPlayerLoginEvent event = new HASPlayerLoginEvent(e.getPlayer(), true);
    // 这次是自动登录
    Bukkit.getPluginManager().callEvent(event);
    if (event.isCancelled()) {
        return;
    }
    // ... 后略
}

这样我们的自定义事件就做好啦!以后就可以通过 @EventHandler 来监听它了啦~

要注意的一点是,callEvent 方法应当尽可能的开新线程处理runTask),对于不可取消事件(即丢出去就不用再管的)更是应当如此。

因为谁也不知道这个事件上面挂了些什么处理函数,要是有一个函数阻塞了(如等待数据库),你的 callEvent 就可能会卡在那里,虽然 Bukkit 具有识别这种情况的能力,但这个风险还是尽量避免。

那上面的例子中为什么没有这样做呢?是因为「查询数据库」这一操作本来就已经在异步执行了,并且我们还需要判断事件有没有取消来决定让不让玩家登录,因此我们只好等等。

异步处理的本质就像「你慢慢弄,我来不及了,我先走了,一会再见」,而同步处理就像「快点快点,我等着你在」。在上面的例子中,如果要取消玩家的登录,就必须等待事件处理完成后查看有没有被取消,用异步实现就没有必要了。

API 打包方法

通常情况下我们希望能够让其他开发者能够使用我们的事件,因此我们希望将它包括到 API 中。

不过这个事件是一个类,不是接口啊,看上去也不好注册什么服务,怎么打包呢?

直接将这个类移动到 API 中(本体中需要删除,然后以 API 为依赖,重新导入这个类),编译即可

不需要注册服务,不需要进行任何其它的操作。

为什么呢?

首先,我们创建了这个事件,这个类没有使用本体中的类(仅仅调用了 Bukkit API)。

然后,这个事件在 HarmonyAuth SMART 将来的开发中,相当一段时间内都不会改变。

因此,我们可以直接将这个类打包到 API 中,因为监听事件本质上也不需要知道这个事件怎么触发,只需要知道这个事件的类就行了,我们就将它提供给开发者。

再说说服务和接口的意义吧,AC-2 中的 API 实际上本质就是通过接口和服务将「不变的部分」从「可变的部分」中「解放」出来,既然这个类本身就是「不变的」,并且也和「可变的部分」没有什么关联,那么我们也不需要「解放」,直接打包就可以了。

当然,如果你的事件类中包含了本体中其它的类(或者乃至其它插件的 API),那么那些类中「可变的」部分需要和 AC-2 中一样,抽象成接口,用本体实现,注册服务等等;「不变的」自然也可以直接打包。

实际上也就是「要解放我,先解放我依赖的那些」,就像移植树木需要先把根挖出来一样。


这就是自定义事件的方法了,在后面的插件开发中我们还会见到它的。