Sponge插件命令系统简介
概述
我们假设需要做一个传送命令(这里就姑且叫做TryTeleport吧),我们想要什么呢?
- 输入
/tryteleport notch 64 128 64
将把玩家notch
传送到坐标为(64,128,64)的位置 - 输入
/tryteleport 64 128 64
将把自己传送到坐标为(64,128,64)的位置 - 输入
/tryteleport DIM-1 64 128 64
将把自己传送到坐标为(64,128,64)的下界位置 - 输入
/tryteleport notch 64 128 64 0 0
将把玩家notch
传送到坐标为(64,128,64)的位置,同时后两个参数决定了传送时的姿态坐标(Yaw和Pitch) - 输入
/tryteleport notch ~ ~ ~
将把玩家notch
传送到自己所在的位置 - 输入
/tryteleport @a ~ ~ ~
将把所有玩家传送到自己所在的位置
需求其实挺复杂,但是还不止这些:
- 如果试图传送自己的是服务端控制台,那么将出错
- 如果试图在位置坐标处输入一些不合法的字符串,那么也会出错
- 如果试图传送的玩家不在线,那么将出错
还有呢:
- 输入
/tryteleport
后,按下Tab键会自动轮流弹出世界中所有玩家的名字
还有一项需求:
- 输入
/help tryteleport
后,会得到相关的参数信息,这里我希望它是:
Usage: /tryteleport [<source>] <location> [<yaw> <pitch>]
所以说,定义一个命令,主要定义的就是这四部分:功能、错误处理、自动补全、以及帮助信息。
实际上我们可以注意到,很多情况下处理命令的行为都类似,比方说输入玩家:
- 处理一个字符串作为玩家名称
- 若字符串对应的玩家不在线则出错
- 在按下Tab时自动从世界寻找已有玩家并轮流返回其名称
- 输出的帮助信息中,玩家参数的相应位置应输出
<player>
在软件工程中,遇到重复的代码就需要想想如何合并,开发Sponge插件亦是如此。而(至少据我所知)Bukkit插件没有提供行之有效的手段,所以开发者就需要一遍一遍地写重复的处理命令的代码,如果涉及到选择器等情况问题还会变得更加复杂。Sponge(至少目前看来)很好地解决了这一问题。
本篇Post的内容就是简要介绍Sponge插件的命令系统。
注册命令
注册命令首先需要在插件主类中监听GameStartingServerEvent
。不熟悉插件主类如何书写的可以看这里:
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.CommandCallable;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameStartingServerEvent;
@Inject
private Logger logger;
@Listener
public void onStartingServer(GameStartingServerEvent event)
{
CommandCallable command = // Your command
Sponge.getCommandManager().register(this, command, "tryteleport", "trytp", "ttp");
this.logger.info("Command successfully registered. ");
}
我们首先需要定义一个CommandCallable
,稍后我们会讲到如何生成一个CommandCallable
。然后我们通过调用CommandManager#register
方法完成命令的注册:
- 该方法的第一个参数传入插件的主类对象,通常是
this
- 该方法的第二个参数传入用于执行插件命令的
CommandCallable
- 该方法的第三个参数开始传入插件的名称,通常名称从复杂到简略,第三个参数将作为该命令的主名称,后面的参数将作为该命令的别名或简写
所以说,这里我们设置/tryteleport
、/trytp
、/ttp
三个命令的作用是一样的。在后面我们不会对三种命令加以区分。
定义命令
我们看看CommandCallable
类的JavaDoc就会注意到有足足有六个方法需要实现,当然你也可以自己实现它以实现手动解析,不过这也不是本篇Post的目的。我们需要讲的是CommandCallable
的一个实现:CommandSpec
。
通常情况下,生成一个CommandSpec
的代码是这样子的:
import org.spongepowered.api.command.CommandCallable;
import org.spongepowered.api.command.spec.CommandSpec;
CommandCallable command = CommandSpec.builder()
.executor(/*Command executor*/)
.arguments(/*Command argument specification*/)
.permission(/*Permission needed*/)
.description(/*Command description*/)
.build();
首先,我们生成一个CommandSpec.Builder
用于生成CommandSpec
,然后我们依次调用CommandSpec.Builder
的各个方法用于设置描述(description
)、权限(permission
)、参数(arguments
)、执行器(executor
)等,最后调用build
方法生成一个CommandSpec
。
我们先从命令参数的定义说起。
命令参数
刚刚说到,我们希望的ttp
命令是这个样子的:
/ttp [<source>] <location> [<yaw> <pitch>]
我们分析一下这个命令应该如何解析:
- 首先试着解析玩家,也就是
<source>
,不过这个玩家是可选的,也就是说如果没有解析到玩家(也就是试图解析<location>
的第一个参数失败了),我们就使用输入命令的玩家当作<source>
- 然后试着解析坐标,坐标的形式是连续的三个整数或者世界名称加上连续的三个整数
- 然后试着解析姿态坐标,这里的姿态坐标是可选的,不过这里的可选不大一样,因为如果格式错误应该直接抛出错误,而不是像之前的玩家一样忽略,当然如果姿态坐标不存在,那么是没有问题的
- 解析姿态坐标相当于解析两个浮点数
也就是说整个树形结构是这样的:
\---序列
\---玩家或命令执行者---source
|
\---坐标---location
|
\---可选
\---序列
\---浮点数---yaw
|
\---浮点数---pitch
如果转换成代码是这样子的:
import org.spongepowered.api.command.args.GenericArguments;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColors;
GenericArguments.seq(
GenericArguments.playerOrSource(Text.of(TextColors.GREEN, "source")),
GenericArguments.location(Text.of("location")),
GenericArguments.optional(
GenericArguments.seq(
GenericArguments.doubleNum(Text.of("yaw")),
GenericArguments.doubleNum(Text.of("pitch")))))
这实际上就和我们传入arguments
方法的参数几乎一致了,这里唯一一点区别就是arguments
方法支持变长参数数组,而对于传入的变长参数数组,Sponge会再使用GenericArguments#seq
方法包装一层。
这里我们使用到了文本API中的Text
对象。Text
对象是SpongeAPI用来描述带有Minecraft特殊颜色、样式等的文本的利器。如果想要了解关于文本API更多,可参见这里的说明。这里作为示例,为了说明Minecraft特殊颜色、样式等是可调的,特意把source
设置成了绿色。
GenericArguments
类还提供了很多种处理参数的方式,及一些可能出现的组合,更多内容可以参见这里。
命令执行
executor
方法需要开发者提供一个org.spongepowered.api.command.spec.CommandExecutor
类的对象。该对象只有一个方法需要实现,也就是说我们可以使用Lambda表达式解决这一问题。我们先在主类写一个相应的方法:
import org.spongepowered.api.command.CommandException;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.command.args.CommandContext;
private CommandResult handleCommand(CommandSource src, CommandContext args) throws CommandException
{
// TODO
return CommandResult.success();
}
有两个CommandResult
对象作为返回值很常用:CommandResult.success()
(用于表示命令执行成功)和CommandResult.empty()
(用于表示命令压根没执行)。
如果命令执行失败怎么办?我们可以抛出CommandException
来表明命令执行失败。Sponge会拦住这一异常并作以相应的处理并产生合适的输出。
很好,刚刚定义了source
、location
、yaw
、和pitch
作为命令的参数,那么现在我们怎么拿到这些参数呢?
import java.util.Collection;
import org.spongepowered.api.command.CommandException;
import org.spongepowered.api.command.CommandResult;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.command.args.CommandContext;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColors;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;
private CommandResult handleCommand(CommandSource src, CommandContext args) throws CommandException
{
Collection<Player> players = args.getAll(Text.of(TextColors.GREEN, "source"));
Location<World> target = args.<Location<World>> getOne(Text.of("location")).get();
double yaw = args.<Double> getOne(Text.of("yaw")).orElse(0.0);
double pitch = args.<Double> getOne(Text.of("pitch")).orElse(0.0);
String template = "Player %s in %s will be teleported from (%d, %d, %d) to (%d, %d, %d) in %s, yaw %.1f, pitch %.1f";
for (Player player : players)
{
Location<World> source = player.getLocation();
String sourceWorldName = source.getExtent().getName();
String targetWorldName = target.getExtent().getName();
String playerName = player.getName();
src.sendMessage(Text.of(String.format(template, playerName, sourceWorldName, source.getBlockX(),
source.getBlockY(), source.getBlockZ(), target.getBlockX(), target.getBlockY(), target.getBlockZ(),
targetWorldName, yaw, pitch)));
}
return CommandResult.success();
}
作为示例,我们获取到所有玩家,并模拟传送的行为,也就是在游戏控制台输出相关信息。
我们只用了CommandContext#getAll
和CommandContext#getOne
两个方法以获取玩家、坐标、和两个浮点数。前者返回一个java.util.Collection
,而后者返回一个java.util.Optional
,然后我们就拿来处理了。
就这么简单?没错!就这么简单。完全不需要繁琐的参数处理过程,因为几乎一切Sponge都已经帮你搞定了。
这里有一点需要说明,CommandContext#getOne
方法在发现获取到的参数不止一个时,将返回一个什么对象都不包含的Optional
。
最后我们把这些都放在一起,并加上了description
(为了方便,这里没有设置权限):
import org.spongepowered.api.command.CommandCallable;
import org.spongepowered.api.command.args.GenericArguments;
import org.spongepowered.api.command.spec.CommandSpec;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColors;
CommandCallable command = CommandSpec.builder().executor(this::handleCommand)
.arguments(
GenericArguments.playerOrSource(Text.of(TextColors.GREEN, "source")),
GenericArguments.location(Text.of("location")),
GenericArguments.optional(
GenericArguments.seq(
GenericArguments.doubleNum(Text.of("yaw")),
GenericArguments.doubleNum(Text.of("pitch")))))
.description(Text.of("Try teleport ", TextColors.GREEN, "player", TextColors.RESET, "s. ")).build();
很好,大功告成!这里是整篇Post的相关源代码。
实际测试
我们打开Sponge服务端,先在服务端控制台进行测试:
/ttp ~ ~ ~
,看到了预期的输出:
/ttp DIM1 ~ ~ ~
,试图传送入下界,输出也如预期所想:
<source>
是可选的:
yaw
和pitch
:
总结
本篇Post所述仅仅是SpongeAPI中命令系统的冰山一角,如果想要了解更多关于SpongeAPI中命令系统的知识,感兴趣的读者可以参阅相关的开发文档。