本帖最后由 gooding300 于 2018-9-2 13:37 编辑

0.引言
进行过1.13游戏的玩家都会知道,Mojang从这一版本开始为命令引入了一套自动的Tab补全,举个例子:

当然,在1.13之前的版本,也可以通过手动按下Tab来完成这样的补全

既然用过的人都说好,那么如何让自己的插件中的命令也能支持这样的自动补全呢?
所幸,Bukkit和Spigot为我们引入了相关的支持。

1.新事物
而这个提供了这一功能的接口名为 TabCompleter
顾名思义,这个接口就是用来进行Tab自动补全的,官方将实现了这个接口的类定义为“一个可以提供命令补全建议的类”,我在这里简称自动补全器
这个接口中只有一个方法,名为onTabComplete
  1. public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args);
复制代码

这个方法可以如此介绍的:
在用户输入命令时被调用,请求返回可能的命令补全列表
参数 sender 命令的来源。如果玩家在命令方块中进行补全,将使用的参数是操作的玩家而不是被操作的命令方块。
参数 command 会被执行的命令
参数 alias 所使用的命令别名
参数 args 传递给命令的参数数组,包括尚未补全的参数
返回 可能的命令补全列表,或返回null以使用默认的自动补全功能(玩家名称补全器)。

例如在上图中,我们所看到的“minecraft:heart_of_the_sea”以及“minecraft:hearvy_weighted_pressure_plate”就是命令补全列表,“@p”、“hea”组成的数组就是参数数组。也就是说,借助这个接口,我们可以为自己的命令提供完全自定义的Tab补全了。

2.如何使用
先用一张图表示相关类的继承关系。

一个普通的插件,都会有一个继承(extend)自JavaPlugin的类,这里称其为插件主类
  1. public class TestPlugin extends JavaPlugin {
  2.     public void onEnable() {
  3.         //插件启动
  4.     }

  5.     @Override
  6.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  7.         //处理命令
  8.     }
  9. }
复制代码
抑或将命令的执行部分从插件主类中分离出去,单独实现(implement)CommandExecutor,这里称其为命令类
  1. public class TestCommand implements CommandExecutor {
  2.     @Override
  3.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  4.         //处理命令
  5.     }
  6. }
复制代码

由上文可知,只要一个类实现了TabCompleter,便可以提供自动补全功能,因此:
使用插件主类直接进行命令操作的,只需要覆写(Override)onTabComplete方法即可。
  1. public class TestPlugin extends JavaPlugin {
  2.     public void onEnable() {
  3.         //插件启动
  4.     }

  5.     @Override
  6.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  7.         //处理命令
  8.     }

  9.     @Override
  10.     public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
  11.         //处理命令补全
  12.     }
  13. }
复制代码
使用独立命令类的,请将实现CommandExecutor改为实现TabExecutor(无需修改执行相关的代码),增加onTabComplete方法即可。
  1. public class TestCommand implements TabExecutor {
  2.     @Override
  3.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  4.         //处理命令
  5.     }

  6.     @Override
  7.     public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
  8.         //处理命令补全
  9.     }
  10. }
复制代码

最后,请不要忘记在setExecutor(设置命令执行器)的时候,也同时setTabCompleter(设置自动补全器)。

3.示例
在这个示例中,我们为“sub”这个命令进行了子命令补全
  1. import org.bukkit.command.Command;
  2. import org.bukkit.command.CommandSender;
  3. import org.bukkit.plugin.java.JavaPlugin;

  4. import java.util.ArrayList;
  5. import java.util.Arrays;
  6. import java.util.List;
  7. import java.util.stream.Collectors;

  8. public class TestPlugin extends JavaPlugin {
  9.     private static final String COMMAND_NAME = "sub";
  10.     private String[] subCommands = {"test", "sample", "sam"};//子命令

  11.     public void onEnable() {
  12.         //注册命令对应的执行器
  13.         getCommand(COMMAND_NAME).setExecutor(this);
  14.         //注册命令对应的自动补全器
  15.         //如果注册了执行器,且自动补全器和执行器在同一个类中,Bukkit会自动尝试将执行器实例转换为命令补全器实例
  16.         //因此这里可以注册也可以不注册,若注册则不再尝试类型转换
  17.         getCommand(COMMAND_NAME).setTabCompleter(this);
  18.     }

  19.     @Override
  20.     public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
  21.         //将第二个参数发给命令执行者
  22.         sender.sendMessage(args.length > 0 ? args[0] : "nothing");
  23.         return true;
  24.     }

  25.     @Override
  26.     public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
  27.         //如果不是能够补全的长度,则返回空列表
  28.         if (args.length > 1) return new ArrayList<>();

  29.         //如果此时仅输入了命令"sub",则直接返回所有的子命令
  30.         if (args.length == 0) return Arrays.asList(subCommands);

  31.         //筛选所有可能的补全列表,并返回
  32.         return Arrays.stream(subCommands).filter(s -> s.startsWith(args[0])).collect(Collectors.toList());
  33.     }
  34. }
复制代码

4.进阶
如果需要补全的是一个物品甚至更多的匹配选项该怎么办?
显然,不断的流式操作本质上是遍历,难以满足每输入一个字母都要进行补全的操作。
这时,就可以请上一个有名的数据结构——Trie了,这个数据结构又称为字典树或者前缀树
这个数据结构正好能够解决这一需求,只需要在插件启动的时候生成这样一棵“树”,每次进行补全查询的时候只需要极少的时间(时间复杂度可以从O(n)优化到O(logn))就能获得想要的结果。

教程适用于Spigot/Bukkit服务端,教程内的所有代码均可以自由使用。
希望这个教程能起到抛砖引玉的作用,欢迎提出更好的实践方案,感谢您的耐心阅读