前言

在食用本文前你需要了解 Kotlin语言,及用到的特性——带接收者的 lambda、内联 reified 泛型函数、扩展函数、中缀函数等。当然这些大部分都是在 Kotlin 中想要写出 DSL 语言结构的必要知识。

DSL 构造命令

关于注册命令的写法大部分都是远古判断法或者注解反射法。关于注解反射这个东西,虽然外表看起来简洁美观,但背后十分邪恶,我个人也非常不喜欢。因此我们从 CommandMap 注册命令开始入手。
首先需要拿到 CommandMap,它位于 Server 类中。

1
2
3
4
5
fun getCommandMap(): CommandMap =
Bukkit.getServer().let {
it::class.java.declaredMethods.firstOrNull { it.name == "getCommandMap" }
?.invoke(it) as CommandMap
}

此处没有对非空做出处理,因为我们知道 Server 类中肯定是存在这个东西的。之后我们就要把抽象类 Command 给实现一下他的 execute 方法。在构造 DSL 过程中,我们把这个方法中执行的内容封装成一个 lambda (CommandSender, String, Array<out String>) -> Unit,用 result 作为最终返回结果。我们定义一个类继承 Command ,并封装上下文及 execute 函数。

1
2
3
4
5
6
7
8
class PackingCommand
(name: String, description: String, usageMessage: String,
aliases: List<String>,
private val action: (CommandSender, String, Array<out String>) -> Unit,
private val result: Boolean) : Command(name, description, usageMessage, aliases){
override fun execute(p0: CommandSender, p1: String, p: Array<out String>) =
result.apply { action(p0, p1, p) }
}
  • namedescriptionusageMessagealiases 对应 plugin.yml 下对命令的配置;
  • action 会在 execute 被调用时执行
  • result 对应处理命令时返回的布尔结果(这里在 runtime 前已经固定)

定义完 PackingCommand 后,可以实现其 DSL Builder。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CommandBuilderDsl(val name: String) {
//下面这些成员都带有默认值,因为每项不是必要的,包括 `action`。
var action: (CommandSender, String, Array<out String>) -> Unit =
{ _, _, _ -> true }
private set //不让在外面修改,只能通过提供的函数赋值。
var description: String = ""
var usageMessage: String = ""
val aliases: MutableList<String> = mutableListOf()
var result: Boolean = true

fun action(block: (CommandSender, String, Array<out String>) -> Unit) {
action = block
}
//这里多提供一种赋值方法,因为 `label` 和 `args` 有时候不需要使用,每次都要写
// `{ sender, _, _ -> }` 太麻烦。
fun action(block: (CommandSender) -> Unit) {
action = { sender, _, _ -> block(sender) }
}

fun addAlias(alias: String) = aliases.add(alias)
}

写完 CommandBuilderDsl 后还需要提供一另一个 CommandScope 来构造 Builder,在这之前构造好的 Command 需要存起来,在插件启用时调用,这部分不贴代码了,就是一个 MutableList<PackingCommand>,向外提供注册方法。CommandScope 的代码如下:

1
2
3
4
5
6
7
8
class CommandScopeDsl {
fun command(name: String, block: CommandBuilderDsl.() -> Unit) {
CommandBuilderDsl(name).apply(block).apply {
PackingCommand(this.name, description, usageMessage, aliases, action, result)
.let(CommandHolder::add)
}
}
}

目前 CommandScope 中就一个函数,也可以省略 CommandScope,直接将 command() 改为顶层函数,不过未来肯定还会加东西,而且这样写是不合理的。贴一下前面的注册方法,commands 是那个持有 PackingCommand 的 list:

1
2
3
4
5
fun register(commandMap: CommandMap) {
commands.forEach {
commandMap.register("DemoPlugin", it as Command)
}
}

再加上把 CommandScope 开放出来的函数:

1
2
3
fun buildCommands(block: CommandScope.() -> Unit) {
CommandScope().apply(block)
}

至此就完成了,在插件启用时调用 CommandHolder.register(getCommandMap()) 注册就好。
随便写个命令举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
buildCommands {
command("gg") {
action { sender ->
sender.sendMessage("233")
true
}
}
command("..."){
action{sender->
//...
}
}
}

总结

有几个问题:

  • 命令多了看起来很乱,大括号越写越多
  • 封装性欠缺
  • 无法美观处理子命令

除了第一个问题是 Kotlin DSL 的硬伤外,其余都可以自己慢慢优化、实现。emerald 主要思想来源于本文,可以在 这里 找到我进一步封装的代码。