Skip to content

Latest commit

 

History

History
184 lines (130 loc) · 8.62 KB

7-1.md

File metadata and controls

184 lines (130 loc) · 8.62 KB

7-1 命令补全器

我们之前在 2-4 讲到过命令处理器,但是那个时候的命令处理器还不完善,我们看下面的问题。

当玩家使用命令时,玩家会经常产生这样的疑惑:「这里应该填什么?」并经常因为不知道这一点而频繁写出错误的命令。

但是,我们在玩原版 Minecraft(单人游戏)时,怎么就没遇到这样的问题呢?

因为有自动补全(Auto Complete),当我们键入命令到一部分时,它就会弹出可选的文本或者提示。

以下图片节选自 Minecraft Wiki 镜像,图片本身遵循该站点的许可,请前往该站点阅读其许可协议。

AC

那么既然有这样的功能,我们自然要好好使用啦~

命令补全器的原理

Bukkit 中有一个用来完成这项工作的接口,它是 TabCompleter,和命令处理器时候的剧情很像,我们要实现这个接口,并且重写 onTabComplete 方法,签名如下:

public List<String> onTabComplete(CommandSender sender, Command cmd, String label, String[] args)

这个方法要怎么实现呢?

只要玩家确认了要使用这个命令(输入了命令名称 + 一个空格),每当玩家输入字符或退格时,都会调用一次这个方法

四个参数中的前三个都很好理解,args是什么呢?我们不是要补全参数吗?

args 指的是「到目前为止已经输入的参数」,也就是说,如果用户现在按下 Enter,将发送的参数

我通过日志输出了每次被调用时的 args 内容:

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 频繁被调用,我们也不能使用 clisti 来阻止,而玩家是否已经注册实际上是一个固定的结果,那我们就只在第一次调用时去数据源(数据库或文件)读取数据,并且将它存入缓存,之后每次读取时,都只需要从缓存(内存中)读取数据,速度因而得到提升。

实际上如果要进一步提升速度应当在玩家进入服务器时就开始读取,可以给服务器以读取数据的时间,但考虑到这个影响不太大,我们就不尝试了。

最后注册命令补全器:

Objects.requireNonNull(Bukkit.getPluginCommand("hl")).setTabCompleter(new TabHandler());

命令补全器应当在命令处理器之后注册,以免出现不可预知的错误。

命令处理整合

由于 CommandExecutorTabCompleter 经常一起使用,写两个类很麻烦,按照 Java 规范,implements 后面可以跟多个接口,因此我们可以将 TabHandler 合并到 CommandHandler 中:

package rarityeg.harmonyauthsmart;

import ...;

public class CommandHandler implements CommandExecutor, TabCompleter {
    // onCommand 和  onTabComplete
}

当然,最后注册时还是要分别使用 setExecutorsetTabCompleter 进行注册。

?> 到底怎么回事
Java 中禁止多重继承,即 extends 后面只允许一个类,但有时候确实有多重继承(同时具有多个类的特点)的需求,Java 用接口代替了这项功能,因此 implements 后面可以跟多个接口。
那么多重继承和实现接口有什么区别呢?可以想,多重继承指的是「主要的」,而接口则是「附属的」,比如,「猫」可以选择继承「哺乳动物」或者「宠物」,显然「哺乳动物」是主要的,「宠物」则是附属的。继承决定了「是什么」,实现接口决定了「有什么能力」……大致就是这样的原理。

还没完。

Bukkit 发现经常需要 implements CommandExecutor, TabCompleter,于是将这两个接口集成到了一个接口中,这个接口就是 TabExecutor,所以我们可以把:

public class CommandHandler implements CommandExecutor, TabCompleter

改成:

public class CommandHandler implements TabExecutor

其它部分和原来还是一样的。实现 onCommandonTabComplete 后,在主类中分别注册就可以了。


这样我们就完成了命令补全器,至此,命令部分的内容就已经正式宣告结束了。