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 4ec428e7b..93af9cc96 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 @@ -12,13 +12,8 @@ package net.mamoe.mirai.console.command import net.mamoe.mirai.console.command.description.* -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.message.data.PlainText import net.mamoe.mirai.message.data.SingleMessage -import java.lang.Exception import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.full.* @@ -29,20 +24,37 @@ import kotlin.reflect.full.* * @see register 注册这个指令 */ interface Command { + /** + * 指令名. 需要至少有一个元素. 所有元素都不能带有空格 + */ val names: Array - fun getPrimaryName():String = names[0] - val usage: String val description: String + + /** + * 指令权限 + */ val permission: CommandPermission + + /** + * 为 `true` 时表示 [指令前缀][CommandPrefix] 可选 + */ val prefixOptional: Boolean val owner: CommandOwner + /** + * @param args 指令参数. 可能是 [SingleMessage] 或 [String]. 且已经以 ' ' 分割. + */ suspend fun onCommand(sender: CommandSender, args: Array) } +/** + * 主要指令名. 为 [Command.names] 的第一个元素. + */ +val Command.primaryName: String get() = names[0] + /** * 功能最集中的Commend * 支持且只支持有sub的指令 @@ -55,20 +67,26 @@ interface Command { abstract class CompositeCommand @JvmOverloads constructor( override val owner: CommandOwner, vararg names: String, - override val description: String = "no description available", + description: String = "no description available", override val permission: CommandPermission = CommandPermission.Default, override val prefixOptional: Boolean = false, overrideContext: CommandParserContext = EmptyCommandParserContext ) : Command { - - class IllegalParameterException(message:String): Exception(message) - - + override val description = description.trimIndent() override val names: Array = - names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).toTypedArray() + names.map(String::trim).filterNot(String::isEmpty).map(String::toLowerCase).also { list -> + list.firstOrNull { !it.isValidSubName() }?.let { + error("Name is not valid: $it") + } + }.toTypedArray() + + /** + * [CommandArgParser] 的环境 + */ val context: CommandParserContext = CommandParserContext.Builtins + overrideContext - override lateinit var usage: String + override var usage: String = "" // initialized by subCommand reflection + internal set /** 指定子指令要求的权限 */ @Retention(AnnotationRetention.RUNTIME) @@ -108,6 +126,9 @@ abstract class CompositeCommand @JvmOverloads constructor( ) } + + class IllegalParameterException internal constructor(message: String) : Exception(message) + internal val subCommands: Array by lazy { val buildUsage = StringBuilder(this.description).append(": \n") @@ -121,38 +142,38 @@ abstract class CompositeCommand @JvmOverloads constructor( println() } - val notStatic = function.findAnnotation()==null + val notStatic = function.findAnnotation() == null val overridePermission = function.findAnnotation()//optional - val subDescription = function.findAnnotation()?.description?:"no description available" + val subDescription = function.findAnnotation()?.description ?: "no description available" - if((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true){ + if ((function.returnType.classifier as? KClass<*>)?.isSubclassOf(Boolean::class) != true) { throw IllegalParameterException("Return Type of SubCommand must be Boolean") } val parameter = function.parameters.toMutableList() - if (parameter.isEmpty()){ - throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.getPrimaryName() + " should be ") + if (parameter.isEmpty()) { + throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be ") } - if(notStatic){ + if (notStatic) { parameter.removeAt(0) } - (parameter.removeAt(0)).let {receiver -> + (parameter.removeAt(0)).let { receiver -> if ( receiver.isVararg || ((receiver.type.classifier as? KClass<*>).also { print(it) } ?.isSubclassOf(CommandSender::class) != true) ) { - throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.getPrimaryName() + " should be ") + throw IllegalParameterException("First parameter (receiver for kotlin) for sub commend " + function.name + " from " + this.primaryName + " should be ") } } val commandName = function.findAnnotation()!!.name.map { - if(!it.isValidSubName()){ + if (!it.isValidSubName()) { error("SubName $it is not valid") } it @@ -160,20 +181,22 @@ abstract class CompositeCommand @JvmOverloads constructor( //map parameter val parms = parameter.map { - buildUsage.append("/" + getPrimaryName() + " ") + buildUsage.append("/$primaryName ") - if(it.isVararg){ - throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.getPrimaryName() + " should not be var arg") + if (it.isVararg) { + throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var arg") } - if(it.isOptional){ - throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.getPrimaryName() + " should not be var optional") + if (it.isOptional) { + throw IllegalParameterException("parameter for sub commend " + function.name + " from " + this.primaryName + " should not be var optional") } - val argName = it.findAnnotation()?.name?:it.name?:"unknown" + val argName = it.findAnnotation()?.name ?: it.name ?: "unknown" buildUsage.append("<").append(argName).append("> ").append(" ") CommandParam( argName, - (it.type.classifier as? KClass<*>)?: throw IllegalParameterException("unsolved type reference from param " + it.name + " in " + function.name + " from " + this.getPrimaryName())) + (it.type.classifier as? KClass<*>) + ?: throw IllegalParameterException("unsolved type reference from param " + it.name + " in " + function.name + " from " + this.primaryName) + ) }.toTypedArray() buildUsage.append(subDescription).append("\n") @@ -184,9 +207,9 @@ abstract class CompositeCommand @JvmOverloads constructor( subDescription, overridePermission?.permission?.getInstance() ?: permission, onCommand = block { sender: CommandSender, args: Array -> - if(notStatic) { - function.callSuspend(this,sender, *args) as Boolean - }else{ + if (notStatic) { + function.callSuspend(this, sender, *args) as Boolean + } else { function.callSuspend(sender, *args) as Boolean } } @@ -317,7 +340,7 @@ internal fun Any.flattenCommandComponents(): ArrayList { internal inline fun KAnnotatedElement.hasAnnotation(): Boolean = findAnnotation() != null -internal inline fun KClass.getInstance():T { +internal inline fun KClass.getInstance(): T { return this.objectInstance ?: this.createInstance() } 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 b3211d9b5..f7992655c 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 @@ -8,34 +8,67 @@ */ @file:Suppress("NOTHING_TO_INLINE", "unused") -@file:JvmName("CommandManager") +@file:JvmName("CommandManagerKt") package net.mamoe.mirai.console.command import kotlinx.atomicfu.locks.withLock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.SingleMessage +import net.mamoe.mirai.utils.MiraiInternalAPI +/** + * 指令的所有者. + * @see PluginCommandOwner + */ sealed class CommandOwner +@MiraiInternalAPI object TestCommandOwner : CommandOwner() -abstract class PluginCommandOwner(val plugin: Plugin) : CommandOwner() +/** + * 插件指令所有者. 插件只能通过 [PluginCommandOwner] 管理指令. + */ +abstract class PluginCommandOwner(val plugin: Plugin) : CommandOwner() { + init { + if (plugin is CoroutineScope) { // JVM Plugin + plugin.coroutineContext[Job]?.invokeOnCompletion { + this.unregisterAllCommands() + } + } + } +} -// 由前端实现 +/** + * 代表控制台所有者. 所有的 mirai-console 内建的指令都属于 [ConsoleCommandOwner]. + * + * 由前端实现 + */ internal abstract class ConsoleCommandOwner : CommandOwner() /** - * 获取已经注册了的指令列表 + * 获取已经注册了的属于这个 [CommandOwner] 的指令列表. + * @see JCommandManager.getRegisteredCommands Java 方法 */ val CommandOwner.registeredCommands: List get() = InternalCommandManager.registeredCommands.filter { it.owner == this } +/** + * 指令前缀, 如 '/' + * @see JCommandManager.getCommandPrefix Java 方法 + */ @get:JvmName("getCommandPrefix") val CommandPrefix: String get() = InternalCommandManager.COMMAND_PREFIX +/** + * 取消注册所有属于 [this] 的指令 + * @see JCommandManager.unregisterAllCommands Java 方法 + */ fun CommandOwner.unregisterAllCommands() { for (registeredCommand in registeredCommands) { registeredCommand.unregister() @@ -43,10 +76,25 @@ fun CommandOwner.unregisterAllCommands() { } /** - * 注册一个指令. 若此指令已经注册或有已经注册的指令与 [SubCommandDescriptor] 重名, 返回 `false` + * 注册一个指令. + * + * @param override 是否覆盖重名指令. + * + * 若原有指令 P, 其 [Command.names] 为 'a', 'b', 'c'. + * 新指令 Q, 其 [Command.names] 为 'b', 将会覆盖原指令 A 注册的 'b'. + * + * 即注册完成后, 'a' 和 'c' 将会解析到指令 P, 而 'b' 会解析到指令 Q. + * + * @return + * 若已有重名指令, 且 [override] 为 `false`, 返回 `false`; + * 若已有重名指令, 但 [override] 为 `true`, 覆盖原有指令并返回 `true`. + * */ -fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { - if (findDuplicate() != null) return false +@JvmOverloads +fun Command.register(override: Boolean = false): Boolean = InternalCommandManager.modifyLock.withLock { + if (!override) { + if (findDuplicate() != null) return false + } InternalCommandManager.registeredCommands.add(this@register) if (this.prefixOptional) { for (name in this.names) { @@ -54,6 +102,7 @@ fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { } } else { for (name in this.names) { + InternalCommandManager.optionalPrefixCommandMap.remove(name) // ensure resolution consistency InternalCommandManager.requiredPrefixCommandMap[name] = this } } @@ -61,13 +110,13 @@ fun Command.register(): Boolean = InternalCommandManager.modifyLock.withLock { } /** - * 查找是否有重名的指令. 返回重名的指令. + * 查找并返回重名的指令. 返回重名指令. */ fun Command.findDuplicate(): Command? = InternalCommandManager.registeredCommands.firstOrNull { it.names intersects this.names } /** - * 取消注册这个指令. 若指令未注册, 返回 `false` + * 取消注册这个指令. 若指令未注册, 返回 `false`. */ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { InternalCommandManager.registeredCommands.remove(this) @@ -78,6 +127,8 @@ fun Command.unregister(): Boolean = InternalCommandManager.modifyLock.withLock { /** * 解析并执行一个指令 * + * Java 调用方式: ` CommandManager.executeCommand(Command)` + * * @param messages 接受 [String] 或 [Message], 其他对象将会被 [Any.toString] * @return 是否成功解析到指令. 返回 `false` 代表无任何指令匹配 */ @@ -88,6 +139,7 @@ suspend fun CommandSender.executeCommand(vararg messages: Any): Boolean { messages[0].let { if (it is SingleMessage) it.toString() else it.toString().substringBefore(' ') }) } +@JvmSynthetic internal inline fun List.dropToTypedArray(n: Int): Array = Array(size - n) { this[n + it] } /** @@ -99,6 +151,7 @@ suspend fun CommandSender.executeCommand(message: MessageChain): Boolean { return executeCommandInternal(message, message[0].toString()) } +@JvmSynthetic internal suspend inline fun CommandSender.executeCommandInternal( messages: Any, commandName: String