mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-11 02:50:15 +08:00
Command execution
This commit is contained in:
parent
258e6ce13a
commit
2dda8a4a7e
@ -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<String>
|
||||
val names: Array<out String>
|
||||
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<String>,
|
||||
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<out Permission>)
|
||||
|
||||
/**
|
||||
* 你应当使用 @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<out Any>) {
|
||||
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<out Any> ->
|
||||
println("default finally got args: ${args.joinToString()}")
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@ -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<out Any>) -> Boolean
|
||||
)
|
||||
|
||||
internal inner class SubCommandDescriptor(
|
||||
val names: Array<String>,
|
||||
val params: Array<CommandParam<*>>,
|
||||
val description: String,
|
||||
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(
|
||||
sender: CommandSender,
|
||||
@ -121,10 +152,11 @@ abstract class CompositeCommand @JvmOverloads constructor(
|
||||
|
||||
@JvmField
|
||||
internal val bakedSubNames: Array<Array<String>> = names.map { it.bakeSubName() }.toTypedArray()
|
||||
private fun parseArgs(sender: CommandSender, rawArgs: Array<out Any>, offset: Int): List<Any> {
|
||||
require(rawArgs.size >= offset + this.params.size) { "No enough args. Required ${params.size}, but given ${rawArgs.size}" }
|
||||
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 - 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<out Any>): SubCommandDescriptor {
|
||||
internal fun matchSubCommand(rawArgs: Array<out Any>): 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 <T> Array<T>.contentEqualsOffset(other: Array<out Any>, offset: Int): Boolean {
|
||||
for (index in other.indices) {
|
||||
if (other[index + offset].toString() != this[index]) {
|
||||
private fun <T> Array<T>.contentEqualsOffset(other: Array<out Any>, length: Int): Boolean {
|
||||
repeat(length) { index ->
|
||||
if (other[index].toString() != this[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -186,12 +218,17 @@ internal fun String.bakeSubName(): Array<String> = split(' ').filterNot { it.isB
|
||||
|
||||
internal fun Any.flattenCommandComponents(): ArrayList<Any> {
|
||||
val list = ArrayList<Any>()
|
||||
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<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())
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified T : Annotation> KAnnotatedElement.hasAnnotation(): Boolean =
|
||||
findAnnotation<T>() != null
|
||||
|
@ -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 <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` 代表无任何指令匹配
|
||||
*/
|
||||
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
|
||||
|
@ -54,7 +54,10 @@ object FloatArgParser : CommandArgParser<Float>() {
|
||||
}
|
||||
|
||||
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>() {
|
||||
|
@ -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 <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? = replacer[klass] ?: this@plus[klass]
|
||||
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 {
|
||||
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 <T : Any> get(klass: KClass<out T>): CommandArgParser<T>? =
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -19,23 +19,6 @@ internal infix fun Array<String>.matchesBeginning(list: List<Any>): 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 <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)
|
||||
for (i in 0 until max) {
|
||||
if (this[i] == other[i]) return true
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user