From ef83281e97ffd4998a88f5c8b68fb906d60f81f6 Mon Sep 17 00:00:00 2001 From: Him188 <Him188@mamoe.net> Date: Fri, 15 May 2020 00:57:50 +0800 Subject: [PATCH] Redesign CommandDescriptor --- .../mamoe/mirai/console/command/Command.kt | 62 +++------------ .../console/command/CommandDescriptor.kt | 46 +++++++---- .../mirai/console/command/CommandManager.kt | 77 +++++++++---------- .../mirai/console/command/CommandParam.kt | 3 + .../console/command/CommandParserContext.kt | 7 +- 5 files changed, 89 insertions(+), 106 deletions(-) diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt index c36120749..fd650dff1 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/Command.kt @@ -11,12 +11,8 @@ package net.mamoe.mirai.console.command -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.mamoe.mirai.console.MiraiConsole -import net.mamoe.mirai.console.plugins.PluginBase +import net.mamoe.mirai.console.command.CommandDescriptor.SubCommandDescriptor import net.mamoe.mirai.message.data.Message -import java.lang.reflect.Member import kotlin.reflect.KProperty internal const val FOR_BINARY_COMPATIBILITY = "for binary compatibility" @@ -42,14 +38,18 @@ interface Command { */ } +internal fun Command.matchChild(args: List<Any>): SubCommandDescriptor { + +} + /** - * 指令实际参数列表. 参数顺序与 [Command.descriptor] 的 [CommandDescriptor.params] 相同. + * 解析完成的指令实际参数列表. 参数顺序与 [Command.descriptor] 的 [CommandDescriptor.params] 相同. */ class CommandArgs private constructor( @JvmField internal val values: List<Any>, - private val fromCommand: Command + private val fromCommand: SubCommandDescriptor ) : List<Any> by values { /** * 获取第一个类型为 [R] 的参数 @@ -71,7 +71,7 @@ class CommandArgs private constructor( * @throws NoSuchElementException 找不到这个名称的参数时抛出 */ operator fun get(name: String?): Any { - val index = fromCommand.descriptor.params.indexOfFirst { it.name == name } + val index = fromCommand.params.indexOfFirst { it.name == name } if (index == -1) { throw NoSuchElementException("Cannot find argument named $name") } @@ -93,12 +93,12 @@ class CommandArgs private constructor( inline operator fun <reified R : Any> getValue(thisRef: Any?, property: KProperty<*>): R = getReified() companion object { - fun parseFrom(command: Command, sender: CommandSender, rawArgs: List<Any>): CommandArgs { - val params = command.descriptor.params + fun parseFrom(command: SubCommandDescriptor, sender: CommandSender, rawArgs: List<Any>): CommandArgs { + val params = command.params require(rawArgs.size >= params.size) { "No enough rawArgs: required ${params.size}, found only ${rawArgs.size}" } - command.descriptor.params.asSequence().zip(rawArgs.asSequence()).map { (commandParam, any) -> + command.params.asSequence().zip(rawArgs.asSequence()).map { (commandParam, any) -> command.parserFor(commandParam)?.parse(any, sender) ?: error("Could not find a parser for param named ${commandParam.name}, typed ${commandParam.type.qualifiedName}") }.toList().let { bakedArgs -> @@ -106,44 +106,4 @@ class CommandArgs private constructor( } } } -} - -inline val Command.fullName get() = descriptor.fullName -inline val Command.usage get() = descriptor.usage -inline val Command.params get() = descriptor.params -inline val Command.description get() = descriptor.description -inline val Command.context get() = descriptor.context -inline val Command.aliases get() = descriptor.aliases -inline val Command.permission get() = descriptor.permission -inline val Command.allNames get() = descriptor.allNames - -abstract class PluginCommand( - final override val owner: PluginBase, - descriptor: CommandDescriptor -) : AbstractCommand(descriptor) - -internal abstract class ConsoleCommand( - descriptor: CommandDescriptor -) : AbstractCommand(descriptor) { - final override val owner: MiraiConsole get() = MiraiConsole -} - -sealed class AbstractCommand( - final override val descriptor: CommandDescriptor -) : Command - - -/** - * For Java - */ -@Suppress("unused") -abstract class BlockingCommand( - owner: PluginBase, - descriptor: CommandDescriptor -) : PluginCommand(owner, descriptor) { - final override suspend fun CommandSender.onCommand(args: CommandArgs): Boolean { - return withContext(Dispatchers.IO) { onCommandBlocking(this@onCommand, args) } - } - - abstract fun onCommandBlocking(sender: CommandSender, args: CommandArgs): Boolean } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt index 43a0b6d84..0a538400b 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandDescriptor.kt @@ -13,7 +13,7 @@ import net.mamoe.mirai.message.data.SingleMessage */ class CommandDescriptor( /** - * 子指令列表 + * 子指令列表. 第一个元素为默认值. */ val subCommands: List<SubCommandDescriptor>, /** @@ -25,8 +25,8 @@ class CommandDescriptor( ) { /** 子指令描述 */ inner class SubCommandDescriptor( - /** 为空字符串时代表默认 */ - val name: String, + /** 子指令名, 如 "/mute group add" 中的 "group add". 当表示默认指令时为父指令名. */ + val subName: String, /** 用法说明 */ val usage: String, /** 指令参数列表, 有顺序. */ @@ -40,8 +40,20 @@ class CommandDescriptor( * @see CommandPermission.or 要求其中一个权限 * @see CommandPermission.and 同时要求两个权限 */ - val permission: CommandPermission = CommandPermission.Default - ) + val permission: CommandPermission = CommandPermission.Default, + /** 指令执行器 */ + val onCommand: suspend (sender: CommandSender, args: CommandArgs) -> Boolean + ) { + init { + require(!(subName.startsWith(' ') || subName.endsWith(' '))) { "subName mustn't start or end with a caret" } + require(subName.isValidSubName()) { "subName mustn't contain any of $ILLEGAL_SUB_NAME_CHARS" } + } + + @JvmField + internal val bakedSubNames: Array<Array<String>> = + listOf(subName, *aliases).map { it.bakeSubName() }.toTypedArray() + internal inline val parent: CommandDescriptor get() = this@CommandDescriptor + } /** * 指令参数解析器环境. @@ -49,6 +61,13 @@ class CommandDescriptor( val context: CommandParserContext = CommandParserContext.Builtins + overrideContext } +internal val CommandDescriptor.base: CommandDescriptor.SubCommandDescriptor get() = subCommands[0] + + +internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray() +internal fun String.isValidSubName(): Boolean = ILLEGAL_SUB_NAME_CHARS.none { it in this } +internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isBlank() }.toTypedArray() + /** * 检查指令参数数量是否足够, 类型是否匹配. @@ -213,15 +232,16 @@ inline class ParamBlock internal constructor(@PublishedApi internal val list: Mu /// internal -internal fun Any.flattenCommandComponents(): Sequence<String> = when (this) { - is Array<*> -> this.asSequence().flatMap { - it?.flattenCommandComponents() ?: throw java.lang.IllegalArgumentException("unexpected null value") +internal fun Any.flattenCommandComponents(): List<String> { + val list = ArrayList<String>() + when (this) { + is String -> list.addAll(split(' ').filterNot { it.isBlank() }) + is PlainText -> list.addAll(content.flattenCommandComponents()) + is SingleMessage -> list.add(this.toString()) + is MessageChain -> this.asSequence().forEach { list.addAll(it.flattenCommandComponents()) } + else -> throw IllegalArgumentException("Illegal component: $this") } - is String -> splitToSequence(' ').filterNot { it.isBlank() } - is PlainText -> content.flattenCommandComponents() - is SingleMessage -> sequenceOf(this.toString()) - is MessageChain -> this.asSequence().flatMap { it.flattenCommandComponents() } - else -> throw IllegalArgumentException("Illegal component: $this") + return list } internal fun Any.checkFullName(errorHint: String): Array<String> { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt index c8b05be87..ec319625a 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt @@ -7,7 +7,7 @@ import kotlinx.atomicfu.locks.withLock import net.mamoe.mirai.console.plugins.PluginBase import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain -import java.util.* +import net.mamoe.mirai.utils.MiraiExperimentalAPI import java.util.concurrent.locks.ReentrantLock sealed class CommandOwner @@ -30,16 +30,13 @@ fun CommandOwner.unregisterAllCommands() { } /** - * 注册一个指令. 若此指令已经注册或有已经注册的指令与 [allNames] 重名, 返回 `false` + * 注册一个指令. 若此指令已经注册或有已经注册的指令与 [SubCommandDescriptor] 重名, 返回 `false` */ fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { if (findDuplicate() != null) { return false } InternalCommandManager.registeredCommands.add(this@register) - for (name in this.allNames) { - InternalCommandManager.nameToCommandMap[name] = this@register - } return true } @@ -47,25 +44,24 @@ fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { * 查找是否有重名的指令. 返回重名的指令. */ fun Command.findDuplicate(): Command? { - return InternalCommandManager.nameToCommandMap.entries.firstOrNull { (names, _) -> - this.allNames.any { it.contentEquals(names) } - }?.value + return InternalCommandManager.registeredCommands.firstOrNull { + it.descriptor.base.bakedSubNames intersects this.descriptor.base.bakedSubNames + } +} + +private infix fun <T> Array<T>.intersects(other: Array<T>): Boolean { + val max = this.size.coerceAtMost(other.size) + for (i in 0 until max) { + if (this[i] == other[i]) return true + } + return false } /** * 取消注册这个指令. 若指令未注册, 返回 `false` */ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { - if (!InternalCommandManager.registeredCommands.contains(this)) { - return false - } - InternalCommandManager.registeredCommands.remove(this) - for (name in this.allNames) { - InternalCommandManager.nameToCommandMap.entries.removeIf { - it.key.contentEquals(this.fullName) - } - } - return true + return InternalCommandManager.registeredCommands.remove(this) } /** @@ -73,8 +69,11 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { * @param args 接受 [String] 或 [Message] * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 */ +@MiraiExperimentalAPI suspend fun CommandSender.executeCommand(vararg args: Any): Boolean { - return args.flattenCommandComponents().toList().executeCommand(this) + val command = InternalCommandManager.matchCommand(args[0].toString()) ?: return false + + return args.flattenCommandComponents().executeCommand(this) } /** @@ -82,15 +81,15 @@ suspend fun CommandSender.executeCommand(vararg args: Any): Boolean { * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 */ suspend fun MessageChain.executeAsCommand(sender: CommandSender): Boolean { - return this.flattenCommandComponents().toList().executeCommand(sender) + return this.flattenCommandComponents().executeCommand(sender) } /** * 检查指令参数并直接执行一个指令. */ -suspend inline fun CommandSender.execute(command: Command, args: CommandArgs): Boolean = with(command) { - checkArgs(args) - return this@execute.onCommand(args) +suspend inline fun CommandSender.execute(command: CommandDescriptor.SubCommandDescriptor, args: CommandArgs): Boolean { + command.checkArgs(args) + return command.onCommand(this@execute, args) } /** @@ -99,19 +98,19 @@ suspend inline fun CommandSender.execute(command: Command, args: CommandArgs): B suspend inline fun Command.execute(sender: CommandSender, args: CommandArgs): Boolean = sender.execute(this, args) /** - * 解析并执行一个指令. - * @param args 接受 [String] 或 [Message] + * 核心执行指令 */ -suspend fun CommandSender.execute(vararg args: Any): Boolean = args.toList().executeCommand(this) - - -internal suspend fun List<Any>.executeCommand(sender: CommandSender): Boolean { - val command = InternalCommandManager.matchCommand(this) ?: return false - return command.run { - sender.onCommand( +internal suspend fun List<Any>.executeCommand(origin: String, sender: CommandSender): Boolean { + if (this.isEmpty()) return false + val command = InternalCommandManager.matchCommand(origin) ?: return false + TODO() + /* + command.descriptor.subCommands.forEach { sub -> + }.run { + sender.onDefault( CommandArgs.parseFrom(command, sender, this@executeCommand.drop(command.fullName.size)) ) - } + }*/ } internal infix fun Array<String>.matchesBeginning(list: List<Any>): Boolean { @@ -125,18 +124,16 @@ internal object InternalCommandManager { @JvmField internal val registeredCommands: MutableList<Command> = mutableListOf() - @JvmField - internal val nameToCommandMap: TreeMap<Array<String>, Command> = TreeMap(Comparator.comparingInt { it.size }) - @JvmField internal val modifyLock = ReentrantLock() internal var _commandPrefix: String = "/" - internal fun matchCommand(splitted: List<Any>): Command? { - nameToCommandMap.entries.forEach { - if (it.key matchesBeginning splitted) return it.value + internal fun matchCommand(name: String): Command? { + return registeredCommands.firstOrNull { command -> + command.descriptor.base.bakedSubNames.any { + name.startsWith(it[0]) && (name.length <= it[0].length || name[it[0].length] == ' ') // 判断跟随空格 + } } - return null } } \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt index b29876c87..2cee303a6 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParam.kt @@ -41,4 +41,7 @@ fun <T : Any> CommandParam<T>.parserFrom(command: Command): CommandArgParser<T>? fun <T : Any> CommandParam<T>.parserFrom(descriptor: CommandDescriptor): CommandArgParser<T>? = descriptor.parserFor(this) +fun <T : Any> CommandParam<T>.parserFrom(descriptor: CommandDescriptor.SubCommandDescriptor): CommandArgParser<T>? = + descriptor.parserFor(this) + fun <T : Any> CommandParam<T>.parserFrom(context: CommandParserContext): CommandArgParser<T>? = context.parserFor(this) \ No newline at end of file diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt index 71ecae55e..c02e017a7 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandParserContext.kt @@ -61,10 +61,13 @@ fun <T : Any> CommandParserContext.parserFor(param: CommandParam<T>): CommandArg param.overrideParser ?: this[param.type] fun <T : Any> CommandDescriptor.parserFor(param: CommandParam<T>): CommandArgParser<T>? = - param.overrideParser ?: this.context.parserFor(param) + this.context.parserFor(param) + +fun <T : Any> CommandDescriptor.SubCommandDescriptor.parserFor(param: CommandParam<T>): CommandArgParser<T>? = + this.parent.parserFor(param) fun <T : Any> Command.parserFor(param: CommandParam<T>): CommandArgParser<T>? = - param.overrideParser ?: this.descriptor.parserFor(param) + this.descriptor.parserFor(param) /** * 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser.