本帖最后由 gooding300 于 2018-9-2 13:37 编辑
0.引言
进行过1.13游戏的玩家都会知道,Mojang从这一版本开始为命令引入了一套自动的Tab补全,举个例子:

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

既然用过的人都说好,那么如何让自己的插件中的命令也能支持这样的自动补全呢?
所幸,Bukkit和Spigot为我们引入了相关的支持。
1.新事物
而这个提供了这一功能的接口名为 TabCompleter 。
顾名思义,这个接口就是用来进行Tab自动补全的,官方将实现了这个接口的类定义为“一个可以提供命令补全建议的类”,我在这里简称自动补全器
这个接口中只有一个方法,名为onTabComplete。复制代码
这个方法可以如此介绍的:
例如在上图中,我们所看到的“minecraft:heart_of_the_sea”以及“minecraft:hearvy_weighted_pressure_plate”就是命令补全列表,“@p”、“hea”组成的数组就是参数数组。也就是说,借助这个接口,我们可以为自己的命令提供完全自定义的Tab补全了。
2.如何使用
先用一张图表示相关类的继承关系。

一个普通的插件,都会有一个继承(extend)自JavaPlugin的类,这里称其为插件主类。复制代码抑或将命令的执行部分从插件主类中分离出去,单独实现(implement)CommandExecutor,这里称其为命令类。
复制代码
由上文可知,只要一个类实现了TabCompleter,便可以提供自动补全功能,因此:
使用插件主类直接进行命令操作的,只需要覆写(Override)onTabComplete方法即可。复制代码使用独立命令类的,请将实现CommandExecutor改为实现TabExecutor(无需修改执行相关的代码),增加onTabComplete方法即可。
复制代码
最后,请不要忘记在setExecutor(设置命令执行器)的时候,也同时setTabCompleter(设置自动补全器)。
3.示例
在这个示例中,我们为“sub”这个命令进行了子命令补全。
复制代码
4.进阶
如果需要补全的是一个物品甚至更多的匹配选项该怎么办?
显然,不断的流式操作本质上是遍历,难以满足每输入一个字母都要进行补全的操作。
这时,就可以请上一个有名的数据结构——Trie了,这个数据结构又称为字典树或者前缀树。
这个数据结构正好能够解决这一需求,只需要在插件启动的时候生成这样一棵“树”,每次进行补全查询的时候只需要极少的时间(时间复杂度可以从O(n)优化到O(logn))就能获得想要的结果。
教程适用于Spigot/Bukkit服务端,教程内的所有代码均可以自由使用。
希望这个教程能起到抛砖引玉的作用,欢迎提出更好的实践方案,感谢您的耐心阅读。
0.引言
进行过1.13游戏的玩家都会知道,Mojang从这一版本开始为命令引入了一套自动的Tab补全,举个例子:

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

既然用过的人都说好,那么如何让自己的插件中的命令也能支持这样的自动补全呢?
所幸,Bukkit和Spigot为我们引入了相关的支持。
1.新事物
而这个提供了这一功能的接口名为 TabCompleter 。
顾名思义,这个接口就是用来进行Tab自动补全的,官方将实现了这个接口的类定义为“一个可以提供命令补全建议的类”,我在这里简称自动补全器
这个接口中只有一个方法,名为onTabComplete。
- 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的类,这里称其为插件主类。
- public class TestPlugin extends JavaPlugin {
- public void onEnable() {
- //插件启动
- }
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- //处理命令
- }
- }
- public class TestCommand implements CommandExecutor {
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- //处理命令
- }
- }
由上文可知,只要一个类实现了TabCompleter,便可以提供自动补全功能,因此:
使用插件主类直接进行命令操作的,只需要覆写(Override)onTabComplete方法即可。
- public class TestPlugin extends JavaPlugin {
- public void onEnable() {
- //插件启动
- }
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- //处理命令
- }
- @Override
- public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
- //处理命令补全
- }
- }
- public class TestCommand implements TabExecutor {
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- //处理命令
- }
- @Override
- public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
- //处理命令补全
- }
- }
最后,请不要忘记在setExecutor(设置命令执行器)的时候,也同时setTabCompleter(设置自动补全器)。
3.示例
在这个示例中,我们为“sub”这个命令进行了子命令补全。
- import org.bukkit.command.Command;
- import org.bukkit.command.CommandSender;
- import org.bukkit.plugin.java.JavaPlugin;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- import java.util.stream.Collectors;
- public class TestPlugin extends JavaPlugin {
- private static final String COMMAND_NAME = "sub";
- private String[] subCommands = {"test", "sample", "sam"};//子命令
- public void onEnable() {
- //注册命令对应的执行器
- getCommand(COMMAND_NAME).setExecutor(this);
- //注册命令对应的自动补全器
- //如果注册了执行器,且自动补全器和执行器在同一个类中,Bukkit会自动尝试将执行器实例转换为命令补全器实例
- //因此这里可以注册也可以不注册,若注册则不再尝试类型转换
- getCommand(COMMAND_NAME).setTabCompleter(this);
- }
- @Override
- public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
- //将第二个参数发给命令执行者
- sender.sendMessage(args.length > 0 ? args[0] : "nothing");
- return true;
- }
- @Override
- public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
- //如果不是能够补全的长度,则返回空列表
- if (args.length > 1) return new ArrayList<>();
- //如果此时仅输入了命令"sub",则直接返回所有的子命令
- if (args.length == 0) return Arrays.asList(subCommands);
- //筛选所有可能的补全列表,并返回
- return Arrays.stream(subCommands).filter(s -> s.startsWith(args[0])).collect(Collectors.toList());
- }
- }
4.进阶
如果需要补全的是一个物品甚至更多的匹配选项该怎么办?
显然,不断的流式操作本质上是遍历,难以满足每输入一个字母都要进行补全的操作。
这时,就可以请上一个有名的数据结构——Trie了,这个数据结构又称为字典树或者前缀树。
这个数据结构正好能够解决这一需求,只需要在插件启动的时候生成这样一棵“树”,每次进行补全查询的时候只需要极少的时间(时间复杂度可以从O(n)优化到O(logn))就能获得想要的结果。
教程适用于Spigot/Bukkit服务端,教程内的所有代码均可以自由使用。
希望这个教程能起到抛砖引玉的作用,欢迎提出更好的实践方案,感谢您的耐心阅读。
本帖最后由 754503921 于 2018-9-2 03:02 编辑
这玩意儿真的是 1.13 引入的吗
这个接口我记得1.7.10的bukkit就有了,只不过彼时的Minecraft需要手按tab键补全
还有这句话什么意思 QAQ
我想学学新的,比如新版本的命令参数类型,指定某个参数只能为int或者玩家啥的
33dalao请务必讲一讲
tab补全请求我觉得可以丢给另一个线程处理了直接发一个包回去,嗯,请也讲一讲
而这个在1.13被引入,提供了这一功能的接口名为 TabCompleter 。
这玩意儿真的是 1.13 引入的吗
这个接口我记得1.7.10的bukkit就有了,只不过彼时的Minecraft需要手按tab键补全
不断的流式操作纵然有多核运算的加持
还有这句话什么意思 QAQ
我想学学新的,比如新版本的命令参数类型,指定某个参数只能为int或者玩家啥的
33dalao请务必讲一讲
tab补全请求我觉得可以丢给另一个线程处理了直接发一个包回去,嗯,请也讲一讲
本帖最后由 gooding300 于 2018-9-2 14:06 编辑
非常感谢您的建议,已针对您提出的问题再次查看了相关源码并进行了修正。
1.经过搜索,确实在1.7.10的Bukkit文档中找到了这个类,已修改相关内容。
2.之前对Stream API的并行处理有所误解,未考虑到并行处理不能保证顺序,不适用于补全操作,现已修正。
3.经过查看Bukkit相关源码,仅发现了玩家名字补全器,原版其他类型的补全器应该尚未加入。
4.我觉得应该没有必要多线程处理,在Trie中匹配的时间复杂度仅为O(logn),即使有一亿个待匹配项,只要限定最终的匹配项数量(例如只有一个字母的时候不匹配,否则网络也是个瓶颈),也可以在1ms内处理完毕。
754503921 发表于 2018-9-2 02:52
这玩意儿真的是 1.13 引入的吗
这个接口我记得1.7.10的bukkit就有了,只不过彼时的Minecraft需要手按tab键 ...
非常感谢您的建议,已针对您提出的问题再次查看了相关源码并进行了修正。
1.经过搜索,确实在1.7.10的Bukkit文档中找到了这个类,已修改相关内容。
2.之前对Stream API的并行处理有所误解,未考虑到并行处理不能保证顺序,不适用于补全操作,现已修正。
3.经过查看Bukkit相关源码,仅发现了玩家名字补全器,原版其他类型的补全器应该尚未加入。
4.我觉得应该没有必要多线程处理,在Trie中匹配的时间复杂度仅为O(logn),即使有一亿个待匹配项,只要限定最终的匹配项数量(例如只有一个字母的时候不匹配,否则网络也是个瓶颈),也可以在1ms内处理完毕。
之前对Stream API的并行处理有所误解,未考虑到并行处理不能保证顺序
为啥是并行处理,Arrays.stream 返回的应该不是一个并行流,并且要顺序的话 sorted() 就行
至于多线程处理那个,假如我想做个插件搜索引擎(我有病),让百度补全,能不能在取得结果后直接向客户端发送补全包
原版其他的补全器肯定是没有的,但是我们水桶开发者应该非常熟悉搞nms的操作了,你就研究下呗
怎么补全指令呢?好像只能补全参数的样子。
后排表示支持。
厉害了,楼主
谢谢楼主,终于不用记插件命令了
楼主我爱你
- ……
- @Override
- public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
- //如果不是能够补全的长度,则返回空列表
- if (args.length > 1) return new ArrayList<>();
- //如果此时仅输入了命令"sub",则直接返回所有的子命令
- if (args.length == 0) return Arrays.asList(subCommands);
- //筛选所有可能的补全列表,并返回
- return Arrays.stream(subCommands).filter(s -> s.startsWith(args[0])).collect(Collectors.toList());
- }
- }
你示例这个部分,如果在输入命令“sub”后加个空格,实际上args.length应该是1而不是0,因为空格打下去后就有一个数组成员了,只不过这个成员的内容是空的
所以按照你这个示例写的话,仅输入命令sub只会返回一个空的数组,改成“args.length == 1”就能正常返回了
感谢大佬的分享!