Command execution

This commit is contained in:
Him188 2020-05-17 00:56:19 +08:00
parent 258e6ce13a
commit 2dda8a4a7e
8 changed files with 148 additions and 77 deletions

View File

@ -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.CommandParam
import net.mamoe.mirai.console.command.description.CommandParserContext 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.command.description.plus
import net.mamoe.mirai.console.plugins.MyArg
import net.mamoe.mirai.message.data.PlainText import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.SingleMessage import net.mamoe.mirai.message.data.SingleMessage
import kotlin.reflect.KAnnotatedElement
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.findAnnotation
/** /**
* 指令 * 指令
@ -24,7 +29,8 @@ import kotlin.reflect.KClass
* @see register 注册这个指令 * @see register 注册这个指令
*/ */
interface Command { interface Command {
val names: Array<String> val names: Array<out String>
val usage: String
val description: String val description: String
val permission: CommandPermission val permission: CommandPermission
val prefixOptional: Boolean val prefixOptional: Boolean
@ -45,51 +51,70 @@ interface Command {
*/ */
abstract class CompositeCommand @JvmOverloads constructor( abstract class CompositeCommand @JvmOverloads constructor(
override val owner: CommandOwner, override val owner: CommandOwner,
override val names: Array<String>, override vararg val names: String,
override val description: String, override val description: String,
override val permission: CommandPermission = CommandPermission.Default,
override val prefixOptional: Boolean = false, override val prefixOptional: Boolean = false,
overrideContext: CommandParserContext overrideContext: CommandParserContext = EmptyCommandParserContext
) : Command { ) : Command {
val context: CommandParserContext = CommandParserContext.Builtins + overrideContext val context: CommandParserContext = CommandParserContext.Builtins + overrideContext
override val usage: String by lazy { TODO() }
/** /** 指定子指令要求的权限 */
* Permission of the command @Retention(AnnotationRetention.RUNTIME)
*/
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
annotation class Permission(val permission: KClass<out Permission>) annotation class Permission(val permission: KClass<out Permission>)
/** /** 标记一个函数为子指令 */
* 你应当使用 @SubCommand 来注册 sub 指令 @Retention(AnnotationRetention.RUNTIME)
*/
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
annotation class SubCommand(val name: String) annotation class SubCommand(val name: String)
/** 指令描述 */
/** @Retention(AnnotationRetention.RUNTIME)
* Usage of the sub command
* you should not include arg names, which will be insert automatically
*/
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
annotation class Usage(val usage: String) annotation class Description(val description: String)
/** /** 参数名, 将参与构成 [usage] */
* name of the parameter @Retention(AnnotationRetention.RUNTIME)
*
* by default available
*/
@Target(AnnotationTarget.VALUE_PARAMETER) @Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Name(val name: String) annotation class Name(val name: String)
final override suspend fun onCommand(sender: CommandSender, args: Array<out Any>) { final override suspend fun onCommand(sender: CommandSender, args: Array<out Any>) {
matchSubCommand(args).parseAndExecute(sender, args) matchSubCommand(args)?.parseAndExecute(sender, args) ?: kotlin.run {
defaultSubCommand.onCommand(sender, args)
}
subCommands subCommands
} }
internal val defaultSubCommand: SubCommandDescriptor by lazy { internal val defaultSubCommand: DefaultSubCommandDescriptor by lazy {
TODO() DefaultSubCommandDescriptor(
"",
CommandPermission.Default,
onCommand = block { sender: CommandSender, args: Array<out Any> ->
println("default finally got args: ${args.joinToString()}")
true
}
)
} }
internal val subCommands: Array<SubCommandDescriptor> by lazy { internal val subCommands: Array<SubCommandDescriptor> by lazy {
TODO() this::class.declaredFunctions.filter { it.hasAnnotation<SubCommand>() }.map { function ->
SubCommandDescriptor(
arrayOf(function.name),
arrayOf(CommandParam("p", MyArg::class)),
"",
CommandPermission.Default,
onCommand = block { sender: CommandSender, args: Array<out Any> ->
println("subname finally gor args: ${args.joinToString()}")
true
}
)
}.toTypedArray()
}
private fun block(block: suspend (CommandSender, Array<out Any>) -> Boolean): suspend (CommandSender, Array<out Any>) -> Boolean {
return block
} }
@JvmField @JvmField
@ -103,12 +128,18 @@ abstract class CompositeCommand @JvmOverloads constructor(
map.toSortedMap(Comparator { o1, o2 -> o1!!.contentHashCode() - o2!!.contentHashCode() }) 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<out Any>) -> Boolean
)
internal inner class SubCommandDescriptor( internal inner class SubCommandDescriptor(
val names: Array<String>, val names: Array<String>,
val params: Array<CommandParam<*>>, val params: Array<CommandParam<*>>,
val description: String, val description: String,
val permission: CommandPermission, val permission: CommandPermission,
val onCommand: suspend (sender: CommandSender, parsedArgs: List<Any>) -> Boolean val onCommand: suspend (sender: CommandSender, parsedArgs: Array<out Any>) -> Boolean
) { ) {
internal suspend inline fun parseAndExecute( internal suspend inline fun parseAndExecute(
sender: CommandSender, sender: CommandSender,
@ -121,10 +152,11 @@ abstract class CompositeCommand @JvmOverloads constructor(
@JvmField @JvmField
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray() internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): List<Any> { private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): Array<out Any> {
require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size}" } 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] val rawArg = rawArgs[offset + index]
when (rawArg) { when (rawArg) {
is String -> context[param.type]?.parse(rawArg, sender) is String -> context[param.type]?.parse(rawArg, sender)
@ -138,18 +170,18 @@ abstract class CompositeCommand @JvmOverloads constructor(
/** /**
* @param rawArgs 元素类型必须为 [SingleMessage] [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException] * @param rawArgs 元素类型必须为 [SingleMessage] [String], 且已经经过扁平化处理. 否则抛出异常 [IllegalArgumentException]
*/ */
internal fun matchSubCommand(rawArgs: Array<out Any>): SubCommandDescriptor { internal fun matchSubCommand(rawArgs: Array<out Any>): SubCommandDescriptor? {
val maxCount = rawArgs.size - 1 val maxCount = rawArgs.size - 1
var cur = 0 var cur = 0
bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) -> bakedCommandNameToSubDescriptorArray.forEach { (name, descriptor) ->
if (name.size != cur) { 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 descriptor
} }
} }
return defaultSubCommand return null
} }
} }
@ -171,9 +203,9 @@ abstract class RawCommand(
} }
private fun <T> Array<T>.contentEqualsOffset(other: Array<out Any>, offset: Int): Boolean { private fun <T> Array<T>.contentEqualsOffset(other: Array<out Any>, length: Int): Boolean {
for (index in other.indices) { repeat(length) { index ->
if (other[index + offset].toString() != this[index]) { if (other[index].toString() != this[index]) {
return false return false
} }
} }
@ -186,12 +218,17 @@ internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isB
internal fun Any.flattenCommandComponents(): ArrayList<Any> { internal fun Any.flattenCommandComponents(): ArrayList<Any> {
val list = ArrayList<Any>() val list = ArrayList<Any>()
when (this) { when (this::class.java) {
is String -> list.addAll(split(' ').filterNot { it.isBlank() }) String::class.java -> (this as String).splitToSequence(' ').filterNot { it.isBlank() }.forEach { list.add(it) }
is PlainText -> list.addAll(content.flattenCommandComponents()) PlainText::class.java -> (this as PlainText).content.splitToSequence(' ').filterNot { it.isBlank() }
is SingleMessage -> list.add(this) .forEach { list.add(it) }
is Iterable<*> -> this.asSequence().forEach { if (it != null) list.addAll(it.flattenCommandComponents()) } SingleMessage::class.java -> list.add(this as SingleMessage)
Array<Any>::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()) else -> list.add(this.toString())
} }
return list return list
} }
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
findAnnotation<T>() != null

View File

@ -7,9 +7,12 @@ import kotlinx.atomicfu.locks.withLock
import net.mamoe.mirai.console.plugins.PluginBase import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.SingleMessage
sealed class CommandOwner sealed class CommandOwner
object TestCommandOwner : CommandOwner()
abstract class PluginCommandOwner(plugin: PluginBase) : CommandOwner() abstract class PluginCommandOwner(plugin: PluginBase) : CommandOwner()
// 由前端实现 // 由前端实现
@ -36,6 +39,15 @@ fun CommandOwner.unregisterAllCommands() {
fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock {
if (findDuplicate() != null) return false if (findDuplicate() != null) return false
InternalCommandManager.registeredCommands.add(this@register) 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 return true
} }
@ -60,8 +72,12 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock {
* @param messages 接受 [String] [Message], 其他对象将会被 [Any.toString] * @param messages 接受 [String] [Message], 其他对象将会被 [Any.toString]
* @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配
*/ */
suspend fun CommandSender.executeCommand(vararg messages: Any): Boolean = suspend fun CommandSender.executeCommand(vararg messages: Any): Boolean {
executeCommandInternal(messages) { messages.getOrNull(it) } if (messages.isEmpty()) return false
return executeCommandInternal(
messages,
messages[0].let { if (it is SingleMessage) it.toString() else it.toString().substringBefore(' ') })
}
internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Array(size - n) { this[n + it] } internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Array(size - n) { this[n + it] }
@ -69,14 +85,16 @@ internal inline fun <reified T> List<T>.dropToTypedArray(n: Int): Array<T> = Arr
* 解析并执行一个指令 * 解析并执行一个指令
* @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配
*/ */
suspend fun CommandSender.executeCommand(message: MessageChain): Boolean = suspend fun CommandSender.executeCommand(message: MessageChain): Boolean {
executeCommandInternal(message) { message.getOrNull(it) } if (message.isEmpty()) return false
return executeCommandInternal(message, message[0].toString())
}
internal suspend inline fun CommandSender.executeCommandInternal( internal suspend inline fun CommandSender.executeCommandInternal(
messages: Any, messages: Any,
iterator: (index: Int) -> Any? commandName: String
): Boolean { ): Boolean {
val command = InternalCommandManager.matchCommand(getCommandName(iterator)) ?: return false val command = InternalCommandManager.matchCommand(commandName) ?: return false
val rawInput = messages.flattenCommandComponents() val rawInput = messages.flattenCommandComponents()
command.onCommand(this, rawInput.dropToTypedArray(1)) command.onCommand(this, rawInput.dropToTypedArray(1))
return true return true

View File

@ -54,7 +54,10 @@ object FloatArgParser : CommandArgParser<Float>() {
} }
object StringArgParser : CommandArgParser<String>() { object StringArgParser : CommandArgParser<String>() {
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<Boolean>() { object BooleanArgParser : CommandArgParser<Boolean>() {

View File

@ -54,16 +54,16 @@ interface CommandParserContext {
Bot::class with ExistBotArgParser Bot::class with ExistBotArgParser
Friend::class with ExistFriendArgParser Friend::class with ExistFriendArgParser
}) })
object Empty : CommandParserContext by CustomCommandParserContext(listOf())
} }
object EmptyCommandParserContext : CommandParserContext by CustomCommandParserContext(listOf())
/** /**
* 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser. * 合并两个 [CommandParserContext], [replacer] 将会替换 [this] 中重复的 parser.
*/ */
operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandParserContext { operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandParserContext {
if (replacer == CommandParserContext.Empty) return this if (replacer == EmptyCommandParserContext) return this
if (this == CommandParserContext.Empty) return replacer if (this == EmptyCommandParserContext) return replacer
return object : CommandParserContext { return object : CommandParserContext {
override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? = replacer[klass] ?: this@plus[klass] override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? = replacer[klass] ?: this@plus[klass]
override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList() override fun toList(): List<ParserPair<*>> = replacer.toList() + this@plus.toList()
@ -75,7 +75,7 @@ operator fun CommandParserContext.plus(replacer: CommandParserContext): CommandP
*/ */
operator fun CommandParserContext.plus(replacer: List<ParserPair<*>>): CommandParserContext { operator fun CommandParserContext.plus(replacer: List<ParserPair<*>>): CommandParserContext {
if (replacer.isEmpty()) return this if (replacer.isEmpty()) return this
if (this == CommandParserContext.Empty) return CustomCommandParserContext(replacer) if (this == EmptyCommandParserContext) return CustomCommandParserContext(replacer)
return object : CommandParserContext { return object : CommandParserContext {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? = override fun <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? =

View File

@ -3,14 +3,15 @@
package net.mamoe.mirai.console.command.description package net.mamoe.mirai.console.command.description
import net.mamoe.mirai.console.command.Command 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.KClass
import kotlin.reflect.KParameter
internal fun KParameter.toCommandParam(): CommandParam<*> { internal fun Parameter.toCommandParam(): CommandParam<*> {
val name = getAnnotation(CompositeCommand.Name::class.java)
return CommandParam( return CommandParam(
this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"), name?.name ?: this.name ?: throw IllegalArgumentException("Cannot construct CommandParam from a unnamed param"),
this.type.classifier as? KClass<*> this.type.kotlin
?: throw IllegalArgumentException("Cannot construct CommandParam from a type parameter")
) )
} }

View File

@ -19,23 +19,6 @@ internal infix fun Array<String>.matchesBeginning(list: List<Any>): Boolean {
return true 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 { internal object InternalCommandManager {
const val COMMAND_PREFIX = "/" const val COMMAND_PREFIX = "/"
@ -66,13 +49,13 @@ internal object InternalCommandManager {
*/ */
internal fun matchCommand(rawCommand: String): Command? { internal fun matchCommand(rawCommand: String): Command? {
if (rawCommand.startsWith(COMMAND_PREFIX)) { if (rawCommand.startsWith(COMMAND_PREFIX)) {
return requiredPrefixCommandMap[rawCommand] return requiredPrefixCommandMap[rawCommand.substringAfter(COMMAND_PREFIX)]
} }
return optionalPrefixCommandMap[rawCommand] return optionalPrefixCommandMap[rawCommand]
} }
} }
internal infix fun <T> Array<T>.intersects(other: Array<T>): Boolean { internal infix fun <T> Array<out T>.intersects(other: Array<out T>): Boolean {
val max = this.size.coerceAtMost(other.size) val max = this.size.coerceAtMost(other.size)
for (i in 0 until max) { for (i in 0 until max) {
if (this[i] == other[i]) return true if (this[i] == other[i]) return true

View File

@ -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()
}
}