命令执行器
在传统的 Bukkit 插件开发中,我们通常会使用 Bukkit 的 CommandExecutor
接口来处理命令。
但在某些情况下,我们需要判断命令的发送者是否为玩家,是否拥有某些权限,判断参数等等。
如果一个插件存在多个命令,那么这些判断逻辑就会重复出现在每个命令的处理方法中,这样的代码是非常冗余的。
除此之外,我们也可能还需要处理命令错误,输出帮助信息等等。
UltiTools-API 对原生的 CommandExecutor
接口进行了封装,提供了一个更加简洁的命令处理方式。
创建命令执行器
你只需要继承 AbstractCommandExecutor
类,并重写 handleHelp
方法。这里的 @CmdTarget
和 @CmdExecutor
注解是代表了该命令的目标类型和执行器信息。
import com.ultikits.ultitools.abstracts.AbstractCommendExecutor;
import com.ultikits.ultitools.annotations.command.CmdExecutor;
import com.ultikits.ultitools.annotations.command.CmdTarget;
import org.bukkit.command.CommandSender;
// 命令限制执行者为玩家和控制台
@CmdTarget(CmdTarget.CmdTargetType.BOTH)
@CmdExecutor(
// 命令权限(可选)
permission = "ultikits.example.all",
// 命令描述(可选)
description = "测试指令",
// 命令别称
alias = {"test","ts"},
// 是否手动注册(可选)
manualRegister = false,
// 是否需要OP权限(可选)
requireOp = false
)
public class ExampleCommand extends AbstractCommendExecutor {
@Override
protected void handleHelp(CommandSender sender) {
// 向命令发送者发送帮助信息
}
}
这样你就完成了一个空的什么都不做的命令执行器。这里的 @CmdTarget
和 @CmdExecutor
注解是代表了该命令的发送者类型和执行器信息。我们将在下一节详细介绍这两个注解。
注册命令
和spigot开发一样,有了执行器,就需要去注册它。我们可以在 registerSelf
方法中使用 getCommandManager().register()
方法来注册命令。
如果你的模块存在大量的命令执行器而不想手动注册,也可以使用 UltiTools 提供的自动注册功能,详情可以查看这篇文章。
import com.ultikits.plugin.ultikitsapiexample.context.ContextConfig;
import com.ultikits.ultitools.abstracts.UltiToolsPlugin;
import com.ultikits.ultitools.annotations.ContextEntry;
import com.ultikits.ultitools.annotations.EnableAutoRegister;
import java.io.IOException;
import java.util.List;
public class UltiToolsConnector extends UltiToolsPlugin {
public UltiToolsConnector(String name, String version, List<String> authors, List<String> depend, int loadPriority, String mainClass) {
super(name, version, authors, depend, loadPriority, mainClass);
}
@Override
public boolean registerSelf() throws IOException {
// 注册命令
getCommandManager().register(this, ExampleCommand.class);
return true;
}
@Override
public void unregisterSelf() {
}
@Override
public void reloadSelf() {
super.reloadSelf();
}
}
基于映射的命令执行器
快速上手
假如你的插件拥有一个设置传送点的功能,你希望玩家输入一个带有传送点名称的命令,以此来设立一个传送点。
那么这个命令应该会长这样:/point add name
如果是使用传统的方法,你需要判断参数输入的合法性,发送者以及权限等,如果还有其他功能,你还需要编写一大堆的 switch ... case
和 if ... else
语句,疯狂嵌套。 使得代码可读性变差,提高了维护的难度。(还容易烧干你的脑子)
使用这个方法,你只需要编写最主要的逻辑即可,剩下的交给 UltiTools。
首先你需要创建一个继承了 AbstractCommandExecutor
的执行器类。
接着创建一个名为 addPoint
的方法,并添加你想要的参数:
public void addPoint(@CmdSender Player player, String name) {
...
}
是的,你的每一个功能都使用一个独立的函数,没有多余的判断。
如果你希望拿到的是 Player
对象而不是 CommandSender
对象,那你拿到的就是 Player
,完全不需要判断与转换。你只需要在希望获取发送者对象的参数前面添加 @CmdSender
注解即可。
然后,你只需要添加 @CmdMapping
注解,以便 UltiTools 能够根据输入的命令匹配你的方法:
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, String name) {
...
}
最后,使用 @CmdParam
来绑定命令参数:
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam("name") String name) {
...
}
至此,你只需要注册命令执行器即可完成所有工作。
参数Tab提示补全
每次写完一个命令之后希望给自己的命令添加Tab提示补全,但是又不想写一大堆的代码?
为Tab补全绞尽脑汁判断每个命令的长度和之前的参数来生成一个补全List,这非常容易把人累死。
现在你只需要简单的为每一个参数写一个方法返回补全List即可!这个方法可以被反复利用,所有繁杂的参数数量判断都交给 UltiTools 来完成。
你所需要做的只是在 @CmdParam
注解中添加 suggest
属性,指定一个方法名即可。
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest="listName") String name) {
...
}
public List<String> listName(Player player, Command command, String[] args) {
...
}
UltiTools会首先在当前类中搜索匹配的方法名,并尝试调用此方法。
你的方法可以包含最多三个参数,分别对应的类型是 Player
, Command
和 String[]
,你可以选择任意的参数数量和顺序,但是类型只能是这三种,每种类型一个参数。
Player
代表了发送此命令的玩家,Command
代表了当前的命令,String[]
代表了当前命令的参数。
你的方法需要返回一个 List<String>
类型的值,UltiTools 将会将此值作为补全列表返回给玩家。
TIP
如果你仅仅只是想返回一个简单的提示字符串,那么你只需要在 suggest
字段中写上你想要的字符串即可。这里的字符串也支持i18n国际化。
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player,
@CmdParam(value = "name", suggest="[名称]") String name) {
...
}
TIP
如果你对UltiTools生成的补全列表不满意,你可以重写 suggest
方法,自己生成补全列表。
@Override
protected List<String> suggest(Player player, Command command, String[] strings) {
...
}
@CmdSuggest 注解
如果你希望你的这个补全方法与其他命令类共享,那么你可以创建一个类,将想要复用的方法写在此类下。
在需要使用此类中的方法的类上添加 @CmdSuggest
注解,指定此类的类名即可。
@CmdSuggest({PointSuggest.class})
public class PointCommand extends AbstractCommandExecutor {
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest="listName") String name) {
...
}
}
public class PointSuggest {
public List<String> listName(Player player, Command command, String[] args) {
...
}
}
参数
无参命令
如果该命令无需任何参数,那么只需要将 format
值留空即可
@CmdMapping(format="")
该种命令最多存在一个
不定参数
在一个方法的最后一个参数,允许使用数组类型,你需要在 format
中的最后一个参数添加 ...
, 下面是一个示例:
@CmdMapping(format = "add <name...>")
public void addPoint(@CmdSender Player player, @CmdParam(value = "name...") String[] name) {
...
}
这样在玩家输入 /somecmd add aa bb cc
时,name
就为 ['aa', 'bb', 'cc']
类型解析
在对方法进行传参之前,UltiTools 会根据方法所需参数的类型对命令的可变参数进行转换。
所有的解析器被储存在一个名为 parsers
的 Map 中,你可以使用 getParser()
获取。
对于部分类型,AbstractCommandExecutor
提供了默认的解析器(包括基类与数组):
- String (Java 内建)
- Float (Java 内建)
- Double (Java 内建)
- Integer (Java 内建)
- Short (Java 内建)
- Byte (Java 内建)
- Long (Java 内建)
- OfflinePlayer (Bukkit API)
- Player (Bukkit API)
- Material (Bukkit API)
- UUID (Java 内建)
- Boolean (Java 内建)
如果你希望使用自定义的解析器,那么你需要创建一个可以使用 Function
接口的方法。
支持的解析器类型为 <String, ?>
, 即方法有且仅有一个 String
类型的参数,并返回一个任意类型的值。
public static SomeType toSomeType(String s) {
//do something...
return result;
}
然后在在构造函数中添加该转换器:
public SomeCommand() {
super();
getParsers().put(Arrays.asList(SomeType.class, SomeType[].class), SomeType::toSomeType);
}
WARNING
请同时添加数组类型,否则将无法解析不定参数
权限
方法权限
如果你需要为某一个方法指定权限,你只需要在 @CmdMapping
添加 permission
属性即可
@CmdMapping(..., permission = "point.set.add")
TIP
如果在 @CmdExecutor
定义了权限,那么命令发送者仅需拥有 @CmdExecutor
指定的权限即可。
OP 限定
如果你希望全部方法只能由OP执行,你只需要在 @CmdExecutor
中设置 requireOp
属性为 true
即可
@CmdExecutor(..., requireOp = true)
如果你希望某一个方法只能由OP执行,你只需要在 @CmdMapping
中设置 requireOp
属性为 true
即可
@CmdMapping(..., requireOp = true)
限定发送者
如果你希望为全部方法指定发送者,你只需要在你的类前面添加 @CmdTarget
注解即可。
如果为某一个方法,则在该方法前面添加即可。
@CmdTarget(CmdTarget.CmdTargetType.BOTH)
TIP
如果在类和方法都指定了发送者,则需要同时满足。
异步执行
如果一个命令需要执行比较耗时的任务,你需要在相应的方法前面添加 @RunAsync
:
@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
//do query
}
这将会创建一个新的异步线程来执行该方法,避免在Bukkit主线程中执行而造成阻塞。
由于 Bukkit API 不允许被异步调用,因此如果你需要调用 Bukkit API,你需要新建一个同步执行的Task:
@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
//do query
new BukkitRunnable() {
@Override
public void run() {
//call bukkit api
}
}.runTask(PluginMain.getInstance());
}
命令冷却
如果你不希望一个指令被大量执行而消耗服务器资源,那么你可以在相应的方法前面添加 @CmdCD
:
@CmdCD(60)
参数类型为整数,单位为秒。
冷却结束之前执行该指令将会发送消息:操作频繁,请稍后再试
此限制仅对玩家生效。
执行锁
如果你希望一个命令只能被一条一条地执行,那么可以在相应的方法前面添加 @UsageLimit
注解:
@UsageLimit(ContainConsole = false, value = LimitType.SENDER)
其中 ContainConsole
为是否将限制应用于控制台,value
为限制类型。
可用的类型有:
LimitType.SENDER
限制每个发送者每次只能有一条该指令在执行LimitType.ALL
限制全服只能有一条该指令在执行LimitType.NONE
不作限制
在 LimitType.SENDER
策略下,玩家在上一条该指令执行完毕之前重复执行前将会收到提示:请先等待上一条命令执行完毕!
在 LimitType.ALL
策略下,玩家在服内上一条该指令执行完毕之前重复执行前将会收到提示:请先等待其他玩家发送的命令执行完毕!
传统命令执行器
游戏内命令
如果你希望一个指令只能在游戏内使用(由玩家执行),那么可以继承 AbstractPlayerCommandExecutor
类,并重写 onPlayerCommand
方法。
public class SomeCommands extends AbstractPlayerCommandExecutor {
@Override
protected boolean onPlayerCommand(Command command, String[] strings, Player player) {
// 你的代码
return true;
}
}
除 Player
类型的参数外,该方法与 CommandExecutor#onCommand
方法相同。
如果尝试在控制台执行该命令,将会收到一条错误消息:只有游戏内可以执行这个指令!
如果你希望该指令能够使用 Tab 补全,请看下一节。
命令补全
自Minecraft 1.13起,Bukkit API提供了一个新的 TabCompleter
接口,用于处理命令补全。
UltiTools 对该接口进行了封装,提供了一个更加简洁的命令补全方式。
你只需要继承 AbstractTabExecutor
类,并重写 onTabComplete
方法。
@Override
protected List<String> onPlayerTabComplete(Command command, String[] strings, Player player) {
// 你的代码
return null;
}
除 Player
类型的参数外,该方法与 TabCompleter#onTabComplete
方法相同。
其余的用法与 AbstractPlayerCommandExecutor
类相同。
控制台指令
如果你希望一个指令只能在控制台使用,那么可以继承 AbstractConsoleCommandExecutor
类,并重写 onConsoleCommand
方法。
public class SomeCommands extends AbstractConsoleCommandExecutor {
@Override
protected boolean onConsoleCommand(CommandSender commandSender, Command command, String[] strings) {
// 你的代码
return true;
}
}
该方法与 CommandExecutor#onCommand
方法相同。
如果尝试在游戏内执行该命令,将会收到一条错误消息:只可以在后台执行这个指令!
指令帮助
上述三个类都提供了一个 sendHelpMessage
方法,用于向玩家或控制台发送帮助信息。
sendHelpMessage(CommandSender sender) {
// 你的代码,向玩家发送信息
}
当发送 /somecommand help
指令时,将会调用该方法。
错误处理
你可能会发现,上述三个类的 onCommand 方法都返回了一个 boolean
类型的值。
与原生的 CommandExecutor
接口相同,该值用于表示命令是否执行成功。
当命令执行返回 false
时,将自动向命令发送者提示信息。