# Mirai Console Backend - PluginData [`Plugin`]: ../backend/mirai-console/src/plugin/Plugin.kt [`PluginDescription`]: ../backend/mirai-console/src/plugin/description/PluginDescription.kt [`PluginLoader`]: ../backend/mirai-console/src/plugin/loader/PluginLoader.kt [`PluginManager`]: ../backend/mirai-console/src/plugin/PluginManager.kt [`JvmPluginLoader`]: ../backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt [`JvmPlugin`]: ../backend/mirai-console/src/plugin/jvm/JvmPlugin.kt [`JvmPluginDescription`]: ../backend/mirai-console/src/plugin/jvm/JvmPluginDescription.kt [`AbstractJvmPlugin`]: ../backend/mirai-console/src/plugin/jvm/AbstractJvmPlugin.kt [`KotlinPlugin`]: ../backend/mirai-console/src/plugin/jvm/KotlinPlugin.kt [`JavaPlugin`]: ../backend/mirai-console/src/plugin/jvm/JavaPlugin.kt [`Value`]: ../backend/mirai-console/src/data/Value.kt [`PluginData`]: ../backend/mirai-console/src/data/PluginData.kt [`AbstractPluginData`]: ../backend/mirai-console/src/data/AbstractPluginData.kt [`AutoSavePluginData`]: ../backend/mirai-console/src/data/AutoSavePluginData.kt [`AutoSavePluginConfig`]: ../backend/mirai-console/src/data/AutoSavePluginConfig.kt [`PluginConfig`]: ../backend/mirai-console/src/data/PluginConfig.kt [`PluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt [`MultiFilePluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt#L116 [`MemoryPluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt#L100 [`AutoSavePluginDataHolder`]: ../backend/mirai-console/src/data/PluginDataHolder.kt#L45 [`PluginDataHolder`]: ../backend/mirai-console/src/data/PluginDataHolder.kt [`PluginDataExtensions`]: ../backend/mirai-console/src/data/PluginDataExtensions.kt [`MiraiConsole`]: ../backend/mirai-console/src/MiraiConsole.kt [`MiraiConsoleImplementation`]: ../backend/mirai-console/src/MiraiConsoleImplementation.kt [`Command`]: ../backend/mirai-console/src/command/Command.kt [`CompositeCommand`]: ../backend/mirai-console/src/command/CompositeCommand.kt [`SimpleCommand`]: ../backend/mirai-console/src/command/SimpleCommand.kt [`RawCommand`]: ../backend/mirai-console/src/command/RawCommand.kt [`CommandManager`]: ../backend/mirai-console/src/command/CommandManager.kt [`Annotations`]: ../backend/mirai-console/src/util/Annotations.kt [`ConsoleInput`]: ../backend/mirai-console/src/util/ConsoleInput.kt [`JavaPluginScheduler`]: ../backend/mirai-console/src/plugin/jvm/JavaPluginScheduler.kt [`ResourceContainer`]: ../backend/mirai-console/src/plugin/ResourceContainer.kt [`PluginFileExtensions`]: ../backend/mirai-console/src/plugin/PluginFileExtensions.kt [Kotlin]: https://www.kotlincn.net/ [Java]: https://www.java.com/zh_CN/ [JVM]: https://zh.wikipedia.org/zh-cn/Java%E8%99%9A%E6%8B%9F%E6%9C%BA [JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F) [为什么不支持热加载和卸载插件?]: QA.md#为什么不支持热加载和卸载插件 [使用 AutoService]: QA.md#使用-autoservice Mirai Console 提供支持自动保存的,静态类型插件数据模型。 ### 设计目标 - 源码级静态强类型:避免 `getString()`, `getList()`... - 全自动加载保存:插件仅需在启动时通过一行代码链接自动保存 - 与前端同步修改:在 Android 等图形化前端实现中可以在内存动态同步修改 - 存储扩展性:可使用多种方式存储,无论是文件还是数据库,插件层都使用同一种实现方式 综上,**最小化插件作者在处理数据和配置做的付出**。 *暂无数据库保存支持,但这已经被提上日程。* ## [`Value`] ```kotlin interface Value : ReadWriteProperty { @get:JvmName("get") @set:JvmName("set") var value: T } ``` 表示一个值代理。在 [`PluginData`] 中,除简单数据类型外,值都经过 [`Value`] 包装。 ## [`PluginData`] 一个插件内部的, 对用户隐藏的数据对象。类似于属性名作为键,对应 [`Value`] 作为值的 `Map`。 [`PluginData`] 接口拥有一个基础实现类,[`AbstractPluginData`],默认不支持自动保存,仅存储键值关系及其序列化器。 插件可继承 [`AbstractPluginData`],拥有高自由的实现细节访问权限,并扩展数据结构。 但通常,插件使用 [`AutoSavePluginData`]。 [`AutoSavePluginData`] 监听保存在它之中的值的修改,并在合适的时机在提供的 [`AutoSavePluginDataHolder`] 协程作用域下启动协程保存数据。 ### 使用 `PluginData` 示例在此时比理论更高效。 1. 定义一个单例,继承 `AutoSavePluginData` ```kotlin object MyData : AutoSavePluginData() ``` 2. 使用委托添加属性。所有类型都可以使用同样的‘语法’。 ```kotlin object MyData : AutoSavePluginData() { val value1 by value() // 推断为 Int val value2 by value(0) // 默认值为 0, 推断为 Int var value3 by value(0) // 支持 var,修改会自动保存 val value4: Int by value() // 显式类型和推断类型,你喜欢哪种? val value5: List by value() // 支持 List,Set val value6: MutableList by value() // 可按需使用 Mutable 类型 val value7: List> by value() // 支持嵌套 val value8: Map>> by value() // 支持 Map var value9: List by value() // List、Set 或 Map 同样支持 var。但请注意这是非引用赋值(详见下文)。 } ``` 3. 建立自动保存链接 使用 `PluginDataStorage.load(PluginDataHolder, PluginData)` 即可完成自动保存链接,并读取数据。 对于 [JVM 插件][`JvmPlugin`],可简便地在 `onEnable()` 中使用 `MyData.reload()`(对于上例)。详见 [读取 `PluginData` 或 `PluginConfig`](Plugins.md#读取-plugindata-或-pluginconfig) ### 定义数据模型(Java) *由于 Java 语法局限,为 Kotlin 而设计的 PluginData 在 Java 使用很复杂。* *即使 Mirai Console 为 Java 提供适配器,也强烈推荐 Java 用户在项目中混用 Kotlin 代码来完成数据模型定义。* 参考 [JAutoSavePluginData](../backend/mirai-console/src/data/java/JAutoSavePluginData.kt#L69) ### 非引用赋值 由于实现特殊, 赋值时不会写其引用. 即: ```kotlin val list = ArrayList("A") MyPluginData.list = list // 赋值给 PluginData 的委托属性是非引用的 println(MyPluginData.list) // "[A]" list.add("B") println(list) // "[A, B]" println(MyPluginData.list) // "[A]" // !! 由于 `list` 的引用并未赋值给 `MyPluginData.list`. ``` 另一个更容易出错的示例: ```kotlin // MyPluginData.nestedMap: MutableMap> by value() val newList = MyPluginData.map.getOrPut(1, ::mutableListOf) newList.add(1) // 不会添加到 MyPluginData.nestedMap 中, 因为 `mutableListOf` 创建的 MutableList 被非引用地添加进了 MyPluginData.nestedMap ``` 要解决这种无法自动初始化空集合的问题,请查看 [实验性扩展方法](#实验性扩展方法) 方法 ### 使用自定义可序列化数据类型 在 Kotlin,支持使用 [kotlinx.serialization](https://github.com/kotlin/kotlinx.serialization) 序列化的自定义数据类型。 **Console 使用反射构造自定义数据类型示例**。当自定义数据类型拥有公开无参构造器,或者一个构造器的所有参数都可选时,在使用委托 `by value()` 时可无需提供默认值。 否则,需要提供默认值。(见如下示例) 自定义数据类型定义: ```kotlin @Serializable // kotlinx.serialization.Serializable class CustomA(val str: String) @Serializable class CustomB(val str: String = "") // 参数可选,CustomB 就可以直接被反射构造。 ``` 使用时: ```kotlin object MyData : AutoSavePluginData() { val value1 by value(CustomA("")) // CustomA 不可以通过反射直接构造实例,因为必须提供参数 str。因此要在创建 value 时提供默认值。 val value2: CustomB by value() // CustomB 可以通过反射直接构造实例 } ``` ### (实验性)[扩展方法][`PluginDataExtensions`] 由于非引用赋值特性,在 `PluginData` 中定义的 `Map` 无法使用 `map.getOrPut(..., ::mutableListOf)` 等方法。 为此,Console 提供一些 *映射方法*。 (下文示例省略 `Value` 所在的 `PluginData` 定义) #### (实验性)`Map.withEmptyDefault` ```kotlin fun SerializerAwareValue>>.withEmptyDefault(): SerializerAwareValue>> ``` 创建一个代理对象, 当 `Map.get` 返回 `null` 时先放入一个 `LinkedHashMap`, 再返回这个 `LinkedHashMap`。 示例: ```kotlin val value1 by value>>().withEmptyDefault() ``` 使用时 ```kotlin val v: MutableMap> = MyData.value1[123456] // 此时 Map.get 返回非 null。因为若 MyData 中不存在 123456 对应的值,就先放入一个空 List。 ``` **但是,这种方法不支持多层嵌套**:例如 `Map>>` 内层的 Map 不会被这样处理。 因此此方法仍处于实验性状态。如果你有任何建议,请在 issues 中发起讨论。 #### (实验性)`Map.withDefault` ```kotlin fun SerializerAwareValue>.withDefault(defaultValueComputer: (K) -> V): SerializerAwareValue> ``` 与上述 `Map.withEmptyDefault` 类似。只是把默认值从 `mutableListOf` 换成了 `defaultValueComputer()` **但是,方法命名仍有待确认**:`withDefault` 可能不是最好的命名,因为可能与标准库的 `map.withDefault` 产生歧义(他们行为不同) #### (实验性)`Map.mapKeys` 映射 `Map` 的键。 ```kotlin fun SerializerAwareValue>.mapKeys( oldToNew: (OldK) -> NewK, newToOld: (NewK) -> OldK, ): SerializerAwareValue> ``` 可进一步简化配置的操作。 示例: ```kotlin val value by value>>().withEmptyDefault().mapKeys(Bot::id, Bot::getInstance) ``` 使用时: ```kotlin val bot: Bot = getBot() val list: List = value[bot] value[bot] = listOf() ``` ## [`PluginConfig`] ### [`PluginData`] 与 [`PluginConfig`] 的区别 - [`PluginData`] 表示插件内部的数据,不应该被用户看到。 - [`PluginConfig`] 表示插件的配置,用户可以修改这些配置。 ### 使用 [`PluginConfig`] [`PluginConfig`] 与 [`PluginData`] 用法完全相同。 在上述 [使用 `PluginData`](#使用-plugindata) 的示例中, 将 [`AutoSavePluginData`] 换为 [`AutoSavePluginConfig`] 即可创建一个配置,而不是数据。 在加载时使用 `configInstance.reload()` 或 `JvmPlugin.reloadPluginConfig(configInstance)`。 ## [`PluginDataHolder`] ***注意:这是实验性 API。*** [`PluginData`] 的拥有者。一般用于区分不同插件的不同 [`PluginData`],避免命名冲突。 [`JvmPlugin`] 实现 [`PluginDataHolder`],使用插件名作为保存时的名称。 ## [`PluginDataStorage`] ***注意:这是实验性 API。*** [`PluginData`] 的存储仓库,将 [`PluginData`] 从内存序列化到文件或到数据库,或反之。 内置的实现包含:[`MultiFilePluginDataStorage`], [`MemoryPluginDataStorage`]