我们之前在 2-4 讲到过命令处理器,但是那个时候的命令处理器还不完善,我们看下面的问题。
当玩家使用命令时,玩家会经常产生这样的疑惑:「这里应该填什么?」并经常因为不知道这一点而频繁写出错误的命令。
但是,我们在玩原版 Minecraft(单人游戏)时,怎么就没遇到这样的问题呢?
因为有自动补全(Auto Complete),当我们键入命令到一部分时,它就会弹出可选的文本或者提示。
以下图片节选自 Minecraft Wiki 镜像,图片本身遵循该站点的许可,请前往该站点阅读其许可协议。
那么既然有这样的功能,我们自然要好好使用啦~
Bukkit 中有一个用来完成这项工作的接口,它是 TabCompleter
,和命令处理器时候的剧情很像,我们要实现这个接口,并且重写 onTabComplete
方法,签名如下:
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args)
这个方法要怎么实现呢?
只要玩家确认了要使用这个命令(输入了命令名称 + 一个空格),每当玩家输入字符或退格时,都会调用一次这个方法。
四个参数中的前三个都很好理解,args
是什么呢?我们不是要补全参数吗?
args
指的是「到目前为止已经输入的参数」,也就是说,如果用户现在按下 Enter,将发送的参数。
我通过日志输出了每次被调用时的 args
内容:
我输入了 asdfghjk
后输入了一个空格,然后输入了 asa
以及另一个空格,现在正准备输入第三个参数。当我按下第二个空格(准备输入第三个参数)后,参数长度就变成了三个;同理,当我按下第一个空格后,参数长度就变成了两个。
命令名称的最后一个字母输入后,按下空格的瞬间,onTabComplete
开始被调用,此时的 args
是一个空数组,此后,随着用户的输入,这个数组会有所变化。
那么我们总结出一个规律:
args
的长度是几,用户当前就在输入第几个参数。
据此,我们就可以编写命令补全器了。返回的 List<String>
就是可供玩家选择的参数。
我们还是拿「HarmonyAuth SMART」进行开发吧。
HarmonyAuth SMART 中有两个命令,/hl
和 /iforgot
,其中 /iforgot
没有参数,那么我们就为 HarmonyAuth SMART 编写命令补全器吧。
首先我们创建类 TabHandler
,实现 TabCompleter
,之所以改个名字也是为了避免冲突。
有了上面的知识,唰唰唰就写出来了:
package rarityeg.harmonyauthsmart;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.*;
public class TabHandler implements TabCompleter {
private static final Map<UUID, Boolean> QUERY_BUFFER = new HashMap<>();
@Override
@ParametersAreNonnullByDefault
public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
if (!(sender instanceof Player)) {
// 控制台注册个鬼
return null;
}
if (args.length >= 3) {
// 前两个参数已经输入完成,不继续提示
return null;
}
UUID id = ((Player) sender).getUniqueId();
if (!RuntimeDataManager.hasRestrictUUID(id)) {
return null;
// 已经登录了
}
if (QUERY_BUFFER.containsKey(id)) {
// 如果有缓存,就从缓存中读
if (QUERY_BUFFER.get(id)) {
// 已经注册
if (args.length == 0 || args.length == 1) {
// 没有参数或正在输入第一个参数
return Collections.singletonList("<输入您的密码>");
// 单项列表
}
return null; // 已存在的用户,登录时无需输入第二遍
} else {
if (args.length == 0 || args.length == 1) {
return Collections.singletonList("<设置您的密码>");
}
return Collections.singletonList("<再输入一遍以确认>");
// 尚未注册的用户需要输入第二遍
}
} else {
// 没有缓存,从数据库或文件读
IDataManager idm;
// 获取一个合适的数据处理器
if (HarmonyAuthSMART.instance.getConfig().getBoolean("mysql.enabled") && !HarmonyAuthSMART.dbError) {
idm = new DBDataManager();
} else {
idm = new FileDataManager();
}
if (idm.isExist(id)) {
// 已经注册
QUERY_BUFFER.put(id, true); // 存入缓存
if (args.length == 0 || args.length == 1) {
return Collections.singletonList("<输入您的密码>");
}
return null;
} else { // 没有注册
QUERY_BUFFER.put(id, false);
if (args.length == 0 || args.length == 1) {
return Collections.singletonList("<设置您的密码>");
}
return Collections.singletonList("<再输入一遍以确认>");
}
}
}
}
原理很简单,由于提示用户输入密码时也没什么「可选补全」,因此我们只提示玩家「输入密码」。不过,在实际的命令应用中,可以向玩家提供可选的命令,此时可以使用 Arrays.asList
。
这里还有一个小技巧,由于玩家每输入一个字母都会调用 onTabComplete
,我们必须尽可能快地完成响应,而且由于 onTabComplete
频繁被调用,我们也不能使用 cli
和 sti
来阻止,而玩家是否已经注册实际上是一个固定的结果,那我们就只在第一次调用时去数据源(数据库或文件)读取数据,并且将它存入缓存,之后每次读取时,都只需要从缓存(内存中)读取数据,速度因而得到提升。
实际上如果要进一步提升速度应当在玩家进入服务器时就开始读取,可以给服务器以读取数据的时间,但考虑到这个影响不太大,我们就不尝试了。
最后注册命令补全器:
Objects.requireNonNull(Bukkit.getPluginCommand("hl")).setTabCompleter(new TabHandler());
命令补全器应当在命令处理器之后注册,以免出现不可预知的错误。
由于 CommandExecutor
和 TabCompleter
经常一起使用,写两个类很麻烦,按照 Java 规范,implements
后面可以跟多个接口,因此我们可以将 TabHandler
合并到 CommandHandler
中:
package rarityeg.harmonyauthsmart;
import ...;
public class CommandHandler implements CommandExecutor, TabCompleter {
// onCommand 和 onTabComplete
}
当然,最后注册时还是要分别使用 setExecutor
和 setTabCompleter
进行注册。
?> 到底怎么回事?
Java 中禁止多重继承,即 extends
后面只允许一个类,但有时候确实有多重继承(同时具有多个类的特点)的需求,Java 用接口代替了这项功能,因此 implements
后面可以跟多个接口。
那么多重继承和实现接口有什么区别呢?可以想,多重继承指的是「主要的」,而接口则是「附属的」,比如,「猫」可以选择继承「哺乳动物」或者「宠物」,显然「哺乳动物」是主要的,「宠物」则是附属的。继承决定了「是什么」,实现接口决定了「有什么能力」……大致就是这样的原理。
还没完。
Bukkit 发现经常需要 implements CommandExecutor, TabCompleter
,于是将这两个接口集成到了一个接口中,这个接口就是 TabExecutor
,所以我们可以把:
public class CommandHandler implements CommandExecutor, TabCompleter
改成:
public class CommandHandler implements TabExecutor
其它部分和原来还是一样的。实现 onCommand
,onTabComplete
后,在主类中分别注册就可以了。
这样我们就完成了命令补全器,至此,命令部分的内容就已经正式宣告结束了。