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 6b7fa4fc7..794561ef4 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 @@ -13,10 +13,15 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.description.CommandParam import net.mamoe.mirai.console.command.description.CommandParserContext +import net.mamoe.mirai.console.command.description.EmptyCommandParserContext import net.mamoe.mirai.console.command.description.plus +import net.mamoe.mirai.console.plugins.MyArg import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.SingleMessage +import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.findAnnotation /** * 指令 @@ -24,7 +29,8 @@ import kotlin.reflect.KClass * @see register 注册这个指令 */ interface Command { - val names: Array + val names: Array + val usage: String val description: String val permission: CommandPermission val prefixOptional: Boolean @@ -45,51 +51,70 @@ interface Command { */ abstract class CompositeCommand @JvmOverloads constructor( override val owner: CommandOwner, - override val names: Array, + override vararg val names: String, override val description: String, + override val permission: CommandPermission = CommandPermission.Default, override val prefixOptional: Boolean = false, - overrideContext: CommandParserContext + overrideContext: CommandParserContext = EmptyCommandParserContext ) : Command { val context: CommandParserContext = CommandParserContext.Builtins + overrideContext + override val usage: String by lazy { TODO() } - /** - * Permission of the command - */ + /** 指定子指令要求的权限 */ + @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class Permission(val permission: KClass) - /** - * 你应当使用 @SubCommand 来注册 sub 指令 - */ + /** 标记一个函数为子指令 */ + @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class SubCommand(val name: String) - - /** - * Usage of the sub command - * you should not include arg names, which will be insert automatically - */ + /** 指令描述 */ + @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) - annotation class Usage(val usage: String) + annotation class Description(val description: String) - /** - * name of the parameter - * - * by default available - */ + /** 参数名, 将参与构成 [usage] */ + @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.VALUE_PARAMETER) annotation class Name(val name: String) final override suspend fun onCommand(sender: CommandSender, args: Array) { - matchSubCommand(args).parseAndExecute(sender, args) + matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run { + defaultSubCommand.onCommand(sender, args) + } subCommands } - internal val defaultSubCommand: SubCommandDescriptor by lazy { - TODO() + internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy { + DefaultSubCommandDescriptor( + "", + CommandPermission.Default, + onCommand = block { sender: CommandSender, args: Array -> + println("default finally got args: ${args.joinToString()}") + true + } + ) } + internal val subCommands: Array by lazy { - TODO() + this::class.declaredFunctions.filter { it.hasAnnotation() }.map { function -> + SubCommandDescriptor( + arrayOf(function.name), + arrayOf(CommandParam("p", MyArg::class)), + "", + CommandPermission.Default, + onCommand = block { sender: CommandSender, args: Array -> + println("subname finally gor args: ${args.joinToString()}") + true + } + ) + }.toTypedArray() + } + + private fun block(block: suspend (CommandSender, Array) -> Boolean): suspend (CommandSender, Array) -> Boolean { + return block } @JvmField @@ -103,12 +128,18 @@ abstract class CompositeCommand @JvmOverloads constructor( map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) } + internal inner class DefaultSubCommandDescriptor( + val description: String, + val permission: CommandPermission, + val onCommand: suspend (sender: CommandSender, rawArgs: Array) -> Boolean + ) + internal inner class SubCommandDescriptor( val names: Array, val params: Array>, val description: String, val permission: CommandPermission, - val onCommand: suspend (sender: CommandSender, parsedArgs: List) -> Boolean + val onCommand: suspend (sender: CommandSender, parsedArgs: Array) -> Boolean ) { internal suspend inline fun parseAndExecute( sender: CommandSender, @@ -121,10 +152,11 @@ abstract class CompositeCommand @JvmOverloads constructor( @JvmField internal val bakedSubNames: Array> = names.map { it.bakeSubName() }.toTypedArray() - private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): List { - require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size}" } + private fun parseArgs(sender: CommandSender, rawArgs: Array, offset: Int): Array { + require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size - offset}" } - return this.params.mapIndexed { index, param -> + return Array(this.params.size) { index -> + val param = params[index] val rawArg = rawArgs[offset + index] when (rawArg) { is String -> context[param.type]?.parse(rawArg, sender) @@ -138,18 +170,18 @@ abstract class CompositeCommand @JvmOverloads constructor( /** * @param rawArgs 元素类型必须为 [SingleMessage] 或 [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] */ - internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor { + internal fun matchSubCommand(rawArgs: Array): SubCommandDescriptor? { val maxCount = rawArgs.size - 1 var cur = 0 bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> if (name.size != cur) { - if (cur++ == maxCount) return defaultSubCommand + if (cur++ == maxCount) return null } - if (name.contentEqualsOffset(rawArgs, offset = cur)) { + if (name.contentEqualsOffset(rawArgs, length = cur)) { return descriptor } } - return defaultSubCommand + return null } } @@ -171,9 +203,9 @@ abstract class RawCommand( } -private fun Array.contentEqualsOffset(other: Array, offset: Int): Boolean { - for (index in other.indices) { - if (other[index + offset].toString() != this[index]) { +private fun Array.contentEqualsOffset(other: Array, length: Int): Boolean { + repeat(length) { index -> + if (other[index].toString() != this[index]) { return false } } @@ -186,12 +218,17 @@ internal fun String.bakeSubName(): Array = split(' ').filterNot { it.isB internal fun Any.flattenCommandComponents(): ArrayList { val list = ArrayList() - when (this) { - is String -> list.addAll(split(' ').filterNot { it.isBlank() }) - is PlainText -> list.addAll(content.flattenCommandComponents()) - is SingleMessage -> list.add(this) - is Iterable<*> -> this.asSequence().forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + when (this::class.java) { + String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) } + PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() } + .forEach { list.add(it) } + SingleMessage::class.java -> list.add(this as SingleMessage) + Array::class.java -> (this as Array<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } + Iterable::class.java -> (this as Iterable<*>).forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } else -> list.add(this.toString()) } return list -} \ No newline at end of file +} + +internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = + findAnnotation() != null 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 f5f7b7f46..63facd067 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,9 +7,12 @@ 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 net.mamoe.mirai.message.data.SingleMessage sealed class CommandOwner +object TestCommandOwner : CommandOwner() + abstract class PluginCommandOwner(plugin: PluginBase) : CommandOwner() // 由前端实现 @@ -36,6 +39,15 @@ fun CommandOwner.unregisterAllCommands() { fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { if (findDuplicate() != null) return false InternalCommandManager.registeredCommands.add(this@register) + if (this.prefixOptional) { + for (name in this.names) { + InternalCommandManager.optionalPrefixCommandMap[name] = this + } + } else { + for (name in this.names) { + InternalCommandManager.requiredPrefixCommandMap[name] = this + } + } return true } @@ -60,8 +72,12 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 */ -suspend fun CommandSender.executeCommand(vararg messages: Any): Boolean = - executeCommandInternal(messages) { messages.getOrNull(it) } +suspend fun CommandSender.executeCommand(vararg messages: Any): Boolean { + if (messages.isEmpty()) return false + return executeCommandInternal( + messages, + messages[0].let { if (it is SingleMessage) it.toString() else it.toString().substringBefore(' ') }) +} internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } @@ -69,14 +85,16 @@ internal inline fun List.dropToTypedArray(n: Int): Array = Arr * 解析并执行一个指令 * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 */ -suspend fun CommandSender.executeCommand(message: MessageChain): Boolean = - executeCommandInternal(message) { message.getOrNull(it) } +suspend fun CommandSender.executeCommand(message: MessageChain): Boolean { + if (message.isEmpty()) return false + return executeCommandInternal(message, message[0].toString()) +} internal suspend inline fun CommandSender.executeCommandInternal( messages: Any, - iterator: (index: Int) -> Any? + commandName: String ): Boolean { - val command = InternalCommandManager.matchCommand(getCommandName(iterator)) ?: return false + val command = InternalCommandManager.matchCommand(commandName) ?: return false val rawInput = messages.flattenCommandComponents() command.onCommand(this, rawInput.dropToTypedArray(1)) return true diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt index 4789ac6fd..01f2cdf92 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandArgParserBuiltins.kt @@ -54,7 +54,10 @@ object FloatArgParser : CommandArgParser() { } object StringArgParser : CommandArgParser() { - override fun parse(raw: String, sender: CommandSender): String = raw + override fun parse(raw: String, sender: CommandSender): String { + println("STRING PARSER! $raw") + return raw + } } object BooleanArgParser : CommandArgParser() { diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt index a159304f1..1a9b8c2c6 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CommandParserContext.kt @@ -54,16 +54,16 @@ interface CommandParserContext { Bot::class with ExistBotArgParser Friend::class with ExistFriendArgParser }) - - object Empty : CommandParserContext by CustomCommandParserContext(listOf()) } +object EmptyCommandParserContext : CommandParserContext by CustomCommandParserContext(listOf()) + /** * 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser. */ operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandParserContext { - if (replacer == CommandParserContext.Empty) return this - if (this == CommandParserContext.Empty) return replacer + if (replacer == EmptyCommandParserContext) return this + if (this == EmptyCommandParserContext) return replacer return object : CommandParserContext { override fun get(klass: KClass): CommandArgParser? = replacer[klass] ?: this@plus[klass] override fun toList(): List> = replacer.toList() + this@plus.toList() @@ -75,7 +75,7 @@ operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandP */ operator fun CommandParserContext.plus(replacer: List>): CommandParserContext { if (replacer.isEmpty()) return this - if (this == CommandParserContext.Empty) return CustomCommandParserContext(replacer) + if (this == EmptyCommandParserContext) return CustomCommandParserContext(replacer) return object : CommandParserContext { @Suppress("UNCHECKED_CAST") override fun get(klass: KClass): CommandArgParser? = diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt index 650ff9cb4..b20dad891 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/description/CompositeCommand.CommandParam.kt @@ -3,14 +3,15 @@ package net.mamoe.mirai.console.command.description import net.mamoe.mirai.console.command.Command +import net.mamoe.mirai.console.command.CompositeCommand +import java.lang.reflect.Parameter import kotlin.reflect.KClass -import kotlin.reflect.KParameter -internal fun KParameter.toCommandParam(): CommandParam<*> { +internal fun Parameter.toCommandParam(): CommandParam<*> { + val name = getAnnotation(CompositeCommand.Name::class.java) return CommandParam( - this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), - this.type.classifier as? KClass<*> - ?: throw IllegalArgumentException("Cannot construct CommandParam from a type parameter") + name?.name ?: this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), + this.type.kotlin ) } diff --git a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt index 3e82c7a95..1594fd98c 100644 --- a/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt +++ b/backend/mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/internal.kt @@ -19,23 +19,6 @@ internal infix fun Array.matchesBeginning(list: List): Boolean { return true } -private val SYMBOL_MISSING_CARET = String(byteArrayOf()) - -internal inline fun getCommandName(iterator: (index: Int) -> Any?): String = buildString { - repeat(Int.MAX_VALUE) { index -> - val next = iterator(index) ?: return@buildString - - val str = next.toString() - val before = str.substringBefore(' ', SYMBOL_MISSING_CARET) - if (before === SYMBOL_MISSING_CARET) { - append(str) - } else { - append(before) - return@buildString - } - } -} - internal object InternalCommandManager { const val COMMAND_PREFIX = "/" @@ -66,13 +49,13 @@ internal object InternalCommandManager { */ internal fun matchCommand(rawCommand: String): Command? { if (rawCommand.startsWith(COMMAND_PREFIX)) { - return requiredPrefixCommandMap[rawCommand] + return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX)] } return optionalPrefixCommandMap[rawCommand] } } -internal infix fun Array.intersects(other: Array): Boolean { +internal infix fun Array.intersects(other: Array): Boolean { val max = this.size.coerceAtMost(other.size) for (i in 0 until max) { if (this[i] == other[i]) return true diff --git a/backend/mirai-console/src/test/java/net/mamoe/mirai/console/command/TestCommands.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommands.kt similarity index 100% rename from backend/mirai-console/src/test/java/net/mamoe/mirai/console/command/TestCommands.kt rename to backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestCommands.kt diff --git a/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt new file mode 100644 index 000000000..8782588bd --- /dev/null +++ b/backend/mirai-console/src/test/kotlin/net/mamoe/mirai/console/command/TestComposite.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.command + +import org.junit.jupiter.api.Test + +object TestCompositeCommand : CompositeCommand( + TestCommandOwner, + "name1", "name2", + description = """ + desc + """.trimIndent() +) + + +internal class TestComposite { + + @Test + fun testRegister() { + TestCompositeCommand.register() + } +} \ No newline at end of file