mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-25 15:40:28 +08:00
Implement PluginSetting and its auto-saving;
Introduce PluginFileExtensions; Introduce ResourceContainer; Introduce SettingHolder; Add extensions to JvmPlugin for getSetting; Add Plugin.safeLoader extension to eliminate unchecked casting; Various documentation improvements;
This commit is contained in:
parent
6beed81f40
commit
e76cfe8c69
@ -10,22 +10,44 @@
|
||||
package net.mamoe.mirai.console.plugin
|
||||
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 表示一个 mirai-console 插件.
|
||||
*
|
||||
* @see JvmPlugin
|
||||
* @see PluginDescription 插件描述
|
||||
* @see JvmPlugin Java, Kotlin 或其他 JVM 平台插件
|
||||
* @see PluginFileExtensions 支持文件系统存储的扩展
|
||||
*
|
||||
* @see PluginLoader 插件加载器
|
||||
*/
|
||||
interface Plugin {
|
||||
/**
|
||||
* 所属插件加载器实例
|
||||
* 所属插件加载器实例, 此加载器必须能加载这个 [Plugin].
|
||||
*/
|
||||
val loader: PluginLoader<*, *>
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline val <P : Plugin> P.safeLoader: PluginLoader<P, PluginDescription>
|
||||
get() = this.loader as PluginLoader<P, PluginDescription>
|
||||
|
||||
/**
|
||||
* 支持文件系统存储的扩展.
|
||||
*
|
||||
* @see JvmPlugin
|
||||
*/
|
||||
@MiraiExperimentalAPI("classname is subject to change")
|
||||
interface PluginFileExtensions {
|
||||
/**
|
||||
* 插件数据目录
|
||||
* 数据目录
|
||||
*/
|
||||
val dataFolder: File
|
||||
|
||||
/**
|
||||
* 从数据目录获取一个文件, 若不存在则创建文件.
|
||||
*/
|
||||
@JvmDefault
|
||||
fun file(relativePath: String): File = File(dataFolder, relativePath).apply { createNewFile() }
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
@file:Suppress("unused")
|
||||
@file:Suppress("unused", "INAPPLICABLE_JVM_NAME")
|
||||
|
||||
package net.mamoe.mirai.console.plugin
|
||||
|
||||
@ -20,6 +20,8 @@ import java.io.File
|
||||
* 插件加载器只实现寻找插件列表, 加载插件, 启用插件, 关闭插件这四个功能.
|
||||
*
|
||||
* 有关插件的依赖和已加载的插件列表由 [PluginManager] 维护.
|
||||
*
|
||||
* @see JarPluginLoader Jar 插件加载器
|
||||
*/
|
||||
interface PluginLoader<P : Plugin, D : PluginDescription> {
|
||||
/**
|
||||
@ -29,12 +31,15 @@ interface PluginLoader<P : Plugin, D : PluginDescription> {
|
||||
|
||||
/**
|
||||
* 获取此插件的描述
|
||||
*
|
||||
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如无法读取插件信息等).
|
||||
*/
|
||||
@Throws(PluginLoadException::class)
|
||||
fun getPluginDescription(plugin: P): D
|
||||
@get:JvmName("getPluginDescription")
|
||||
@get:Throws(PluginLoadException::class)
|
||||
val P.description: D
|
||||
|
||||
/**
|
||||
* 加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的实例
|
||||
* 加载一个插件 (实例), 但不 [启用][enable] 它. 返回加载成功的主类实例
|
||||
*
|
||||
* @throws PluginLoadException 在加载插件遇到意料之中的错误时抛出 (如找不到主类等).
|
||||
*/
|
||||
@ -55,7 +60,7 @@ open class PluginLoadException : RuntimeException {
|
||||
/**
|
||||
* '/plugins' 目录中的插件的加载器. 每个加载器需绑定一个后缀.
|
||||
*
|
||||
* @see AbstractFilePluginLoader
|
||||
* @see AbstractFilePluginLoader 默认基础实现
|
||||
* @see JarPluginLoader 内建的 Jar (JVM) 插件加载器.
|
||||
*/
|
||||
interface FilePluginLoader<P : Plugin, D : PluginDescription> : PluginLoader<P, D> {
|
||||
@ -65,6 +70,9 @@ interface FilePluginLoader<P : Plugin, D : PluginDescription> : PluginLoader<P,
|
||||
val fileSuffix: String
|
||||
}
|
||||
|
||||
/**
|
||||
* [FilePluginLoader] 的默认基础实现
|
||||
*/
|
||||
abstract class AbstractFilePluginLoader<P : Plugin, D : PluginDescription>(
|
||||
override val fileSuffix: String
|
||||
) : FilePluginLoader<P, D> {
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
package net.mamoe.mirai.console.plugin.internal
|
||||
|
||||
import kotlinx.atomicfu.AtomicLong
|
||||
import kotlinx.atomicfu.locks.withLock
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -19,8 +20,10 @@ import net.mamoe.mirai.console.plugin.Plugin
|
||||
import net.mamoe.mirai.console.plugin.PluginManager
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPlugin
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
|
||||
import net.mamoe.mirai.console.utils.asResourceContainer
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
@ -31,13 +34,17 @@ internal val <T> T.job: Job where T : CoroutineScope, T : Plugin get() = this.co
|
||||
* Hides implementations from [JvmPlugin]
|
||||
*/
|
||||
@PublishedApi
|
||||
internal abstract class JvmPluginImpl(
|
||||
internal abstract class JvmPluginInternal(
|
||||
parentCoroutineContext: CoroutineContext
|
||||
) : JvmPlugin,
|
||||
CoroutineScope {
|
||||
|
||||
private val resourceConsoleDelegate by lazy { this::class.asResourceContainer() }
|
||||
override fun getResourceAsStream(name: String): InputStream = resourceConsoleDelegate.getResourceAsStream(name)
|
||||
|
||||
// region JvmPlugin
|
||||
/**
|
||||
* Initialized immediately after construction of [JvmPluginImpl] instance
|
||||
* Initialized immediately after construction of [JvmPluginInternal] instance
|
||||
*/
|
||||
@Suppress("PropertyName")
|
||||
internal lateinit var _description: JvmPluginDescription
|
||||
@ -102,4 +109,16 @@ internal abstract class JvmPluginImpl(
|
||||
?: contextUpdateLock.withLock { _coroutineContext ?: refreshCoroutineContext() }
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
internal inline fun AtomicLong.updateWhen(condition: (Long) -> Boolean, update: (Long) -> Long): Boolean {
|
||||
while (true) {
|
||||
val current = value
|
||||
if (condition(current)) {
|
||||
if (compareAndSet(0, update(current))) {
|
||||
return true
|
||||
} else continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -11,7 +11,10 @@
|
||||
|
||||
package net.mamoe.mirai.console.plugin.jvm
|
||||
|
||||
import net.mamoe.mirai.console.plugin.internal.JvmPluginImpl
|
||||
import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal
|
||||
import net.mamoe.mirai.console.setting.Setting
|
||||
import net.mamoe.mirai.console.setting.getValue
|
||||
import net.mamoe.mirai.console.setting.value
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
@ -23,8 +26,16 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
*/
|
||||
abstract class AbstractJvmPlugin @JvmOverloads constructor(
|
||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||
) : JvmPlugin, JvmPluginImpl(parentCoroutineContext) {
|
||||
// TODO: 2020/6/24 添加 PluginSetting 继承 Setting, 实现 onValueChanged 并绑定自动保存.
|
||||
) : JvmPlugin, JvmPluginInternal(parentCoroutineContext) {
|
||||
final override val name: String get() = this.description.name
|
||||
|
||||
abstract class PluginSetting
|
||||
override fun <T : Setting> getSetting(clazz: Class<T>): T = loader.settingStorage.load(this, clazz)
|
||||
}
|
||||
|
||||
|
||||
object MyPlugin : KotlinPlugin()
|
||||
|
||||
object TestSetting : Setting by MyPlugin.getSetting() {
|
||||
val account by value("123456")
|
||||
val password by value("123")
|
||||
}
|
@ -13,8 +13,10 @@ import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.console.MiraiConsole
|
||||
import net.mamoe.mirai.console.plugin.AbstractFilePluginLoader
|
||||
import net.mamoe.mirai.console.plugin.PluginLoadException
|
||||
import net.mamoe.mirai.console.plugin.internal.JvmPluginImpl
|
||||
import net.mamoe.mirai.console.plugin.internal.JvmPluginInternal
|
||||
import net.mamoe.mirai.console.plugin.internal.PluginsLoader
|
||||
import net.mamoe.mirai.console.setting.SettingStorage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.yamlkt.Yaml
|
||||
import java.io.File
|
||||
@ -30,6 +32,9 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
|
||||
MiraiConsole.newLogger(JarPluginLoader::class.simpleName!!)
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
val settingStorage: SettingStorage = TODO()
|
||||
|
||||
override val coroutineContext: CoroutineContext by lazy {
|
||||
MiraiConsole.coroutineContext + SupervisorJob(
|
||||
MiraiConsole.coroutineContext[Job]
|
||||
@ -48,7 +53,9 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPluginDescription(plugin: JvmPlugin): JvmPluginDescription = plugin.description
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER") // doesn't matter
|
||||
override val JvmPlugin.description: JvmPluginDescription
|
||||
get() = this.description
|
||||
|
||||
override fun Sequence<File>.mapToDescription(): List<JvmPluginDescription> {
|
||||
return this.associateWith { URL("jar:${it.absolutePath}!/plugin.yml") }.mapNotNull { (file, url) ->
|
||||
@ -82,7 +89,7 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
|
||||
|
||||
check(main is JvmPlugin) { "The main class of Jar plugin must extend JvmPlugin, recommending JavaPlugin or KotlinPlugin" }
|
||||
|
||||
if (main is JvmPluginImpl) {
|
||||
if (main is JvmPluginInternal) {
|
||||
main._description = description
|
||||
main.internalOnLoad()
|
||||
} else main.onLoad()
|
||||
@ -93,13 +100,13 @@ object JarPluginLoader : AbstractFilePluginLoader<JvmPlugin, JvmPluginDescriptio
|
||||
|
||||
override fun enable(plugin: JvmPlugin) {
|
||||
ensureActive()
|
||||
if (plugin is JvmPluginImpl) {
|
||||
if (plugin is JvmPluginInternal) {
|
||||
plugin.internalOnEnable()
|
||||
} else plugin.onEnable()
|
||||
}
|
||||
|
||||
override fun disable(plugin: JvmPlugin) {
|
||||
if (plugin is JvmPluginImpl) {
|
||||
if (plugin is JvmPluginInternal) {
|
||||
plugin.internalOnDisable()
|
||||
} else plugin.onDisable()
|
||||
}
|
||||
|
@ -22,6 +22,62 @@ abstract class JavaPlugin @JvmOverloads constructor(
|
||||
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||
) : JvmPlugin, AbstractJvmPlugin(parentCoroutineContext) {
|
||||
|
||||
/*
|
||||
|
||||
@Volatile
|
||||
internal var lastAutoSaveJob: Job? = null
|
||||
|
||||
@Volatile
|
||||
internal var currentFirstStartTime = atomic(0L)
|
||||
|
||||
/**
|
||||
* [PluginSetting] 每次自动保存时间间隔
|
||||
*
|
||||
* - 区间的左端点为最小间隔, 一个 [Value] 被修改后, 若此时间段后无其他修改, 将触发自动保存; 若有, 将重新开始计时.
|
||||
* - 区间的右端点为最大间隔, 一个 [Value] 被修改后, 最多不超过这个时间段后就会被保存.
|
||||
*
|
||||
* 备注: 当插件被关闭时, 所有相关 [PluginSetting] 总是会被自动保存.
|
||||
*/
|
||||
open val autoSaveIntervalMillis: LongRange
|
||||
get() = 30.secondsToMillis..10.minutesToSeconds
|
||||
|
||||
/**
|
||||
* 链接自动保存的 [Setting].
|
||||
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
|
||||
*/
|
||||
abstract inner class PluginSetting : AbstractSetting() {
|
||||
init {
|
||||
|
||||
this@AbstractJvmPlugin.job.invokeOnCompletion {
|
||||
doSave()
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onValueChanged(value: Value<*>) {
|
||||
lastAutoSaveJob = launch {
|
||||
currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis })
|
||||
|
||||
delay(autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety
|
||||
|
||||
if (lastAutoSaveJob == job) {
|
||||
doSave()
|
||||
} else {
|
||||
if (currentFirstStartTime.updateWhen(
|
||||
{ currentTimeMillis - it >= autoSaveIntervalMillis.last },
|
||||
{ 0 })
|
||||
) {
|
||||
doSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doSave() {
|
||||
loader.settingStorage.store(this@AbstractJvmPlugin, this@PluginSetting)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Java API Scheduler
|
||||
*/
|
||||
|
@ -13,7 +13,12 @@ package net.mamoe.mirai.console.plugin.jvm
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.mamoe.mirai.console.plugin.Plugin
|
||||
import net.mamoe.mirai.console.plugin.PluginFileExtensions
|
||||
import net.mamoe.mirai.console.setting.Setting
|
||||
import net.mamoe.mirai.console.setting.SettingHolder
|
||||
import net.mamoe.mirai.console.utils.ResourceContainer
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
/**
|
||||
@ -23,8 +28,11 @@ import net.mamoe.mirai.utils.MiraiLogger
|
||||
*
|
||||
* @see JavaPlugin Java 插件
|
||||
* @see KotlinPlugin Kotlin 插件
|
||||
*
|
||||
* @see JvmPlugin 支持文件系统扩展
|
||||
* @see ResourceContainer 支持资源获取 (如 Jar 中的资源文件)
|
||||
*/
|
||||
interface JvmPlugin : Plugin, CoroutineScope {
|
||||
interface JvmPlugin : Plugin, CoroutineScope, PluginFileExtensions, ResourceContainer, SettingHolder {
|
||||
/** 日志 */
|
||||
val logger: MiraiLogger
|
||||
|
||||
@ -34,6 +42,11 @@ interface JvmPlugin : Plugin, CoroutineScope {
|
||||
/** 所属插件加载器实例 */
|
||||
override val loader: JarPluginLoader get() = JarPluginLoader
|
||||
|
||||
/**
|
||||
* 获取一个 [Setting] 实例
|
||||
*/
|
||||
fun <T : Setting> getSetting(clazz: Class<T>): T
|
||||
|
||||
@JvmDefault
|
||||
fun onLoad() {
|
||||
}
|
||||
@ -47,4 +60,5 @@ interface JvmPlugin : Plugin, CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T : Setting> JvmPlugin.getSetting(clazz: KClass<T>) = this.getSetting(clazz.java)
|
||||
inline fun <reified T : Setting> JvmPlugin.getSetting() = this.getSetting(T::class)
|
@ -17,9 +17,23 @@ import kotlin.internal.LowPriorityInOverloadResolution
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KType
|
||||
|
||||
// Shows public APIs such as deciding when to auto-save.
|
||||
abstract class Setting : SettingImpl() {
|
||||
operator fun <T> SerializerAwareValue<T>.provideDelegate(
|
||||
|
||||
/**
|
||||
* 序列化之后的名称.
|
||||
*
|
||||
* 例:
|
||||
* ```
|
||||
* class MySetting : Setting() {
|
||||
*
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
// TODO: 2020/6/26 document
|
||||
typealias SerialName = kotlinx.serialization.SerialName
|
||||
|
||||
// TODO: 2020/6/26 document
|
||||
abstract class AbstractSetting : Setting, SettingImpl() {
|
||||
final override operator fun <T> SerializerAwareValue<T>.provideDelegate(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T> {
|
||||
@ -28,7 +42,19 @@ abstract class Setting : SettingImpl() {
|
||||
return this
|
||||
}
|
||||
|
||||
public override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer
|
||||
final override val updaterSerializer: KSerializer<Unit> get() = super.updaterSerializer
|
||||
}
|
||||
|
||||
// TODO: 2020/6/26 document
|
||||
interface Setting {
|
||||
// TODO: 2020/6/26 document
|
||||
operator fun <T> SerializerAwareValue<T>.provideDelegate(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T>
|
||||
|
||||
// TODO: 2020/6/26 document
|
||||
val updaterSerializer: KSerializer<Unit>
|
||||
}
|
||||
|
||||
//// region Setting_value_primitives CODEGEN ////
|
||||
|
@ -1,3 +1,19 @@
|
||||
package net.mamoe.mirai.console.setting
|
||||
|
||||
interface SettingStorage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalAPI
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface SettingStorage {
|
||||
@JvmDefault
|
||||
fun <T : Setting> load(holder: SettingHolder, settingClass: KClass<T>): T = this.load(holder, settingClass.java)
|
||||
|
||||
fun <T : Setting> load(holder: SettingHolder, settingClass: Class<T>): T
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
fun store(holder: SettingHolder, setting: Setting)
|
||||
}
|
||||
|
||||
@MiraiExperimentalAPI
|
||||
interface SettingHolder {
|
||||
val name: String
|
||||
}
|
@ -14,6 +14,7 @@ package net.mamoe.mirai.console.setting.internal
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import net.mamoe.mirai.console.setting.SerializerAwareValue
|
||||
import net.mamoe.mirai.console.setting.Setting
|
||||
import net.mamoe.mirai.console.setting.Value
|
||||
import net.mamoe.yamlkt.YamlNullableDynamicSerializer
|
||||
@ -36,6 +37,14 @@ internal abstract class SettingImpl {
|
||||
val updaterSerializer: KSerializer<Unit>
|
||||
)
|
||||
|
||||
internal fun <T> SerializerAwareValue<T>.provideDelegateImpl(
|
||||
property: KProperty<*>
|
||||
): SerializerAwareValue<T> {
|
||||
val name = property.serialName
|
||||
valueNodes.add(Node(name, this, this.serializer))
|
||||
return this
|
||||
}
|
||||
|
||||
internal val valueNodes: MutableList<Node<*>> = mutableListOf()
|
||||
|
||||
internal open val updaterSerializer: KSerializer<Unit> = object : KSerializer<Unit> {
|
||||
|
@ -0,0 +1,58 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.mamoe.mirai.console.utils
|
||||
|
||||
import net.mamoe.mirai.console.encodeToString
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* 资源容器.
|
||||
*/
|
||||
interface ResourceContainer {
|
||||
/**
|
||||
* 获取一个资源文件
|
||||
*/
|
||||
fun getResourceAsStream(name: String): InputStream
|
||||
|
||||
/**
|
||||
* 读取一个资源文件并以 [Charsets.UTF_8] 编码为 [String]
|
||||
*/
|
||||
@JvmDefault
|
||||
fun getResource(name: String): String = getResource(name, Charsets.UTF_8)
|
||||
|
||||
/**
|
||||
* 读取一个资源文件并以 [charset] 编码为 [String]
|
||||
*/
|
||||
@JvmDefault
|
||||
fun getResource(name: String, charset: Charset): String =
|
||||
this.getResourceAsStream(name).use { it.readBytes() }.encodeToString()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 使用 [Class.getResourceAsStream] 读取资源文件
|
||||
*
|
||||
* @see asResourceContainer Kotlin 使用
|
||||
*/
|
||||
@JvmStatic
|
||||
@JavaFriendlyAPI
|
||||
fun byClass(clazz: Class<*>): ResourceContainer = clazz.asResourceContainer()
|
||||
}
|
||||
}
|
||||
|
||||
internal class ClassAsResourceContainer(
|
||||
private val clazz: Class<*>
|
||||
) : ResourceContainer {
|
||||
override fun getResourceAsStream(name: String): InputStream = clazz.getResourceAsStream(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 [Class.getResourceAsStream] 读取资源文件
|
||||
*/
|
||||
fun KClass<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this.java)
|
||||
|
||||
/**
|
||||
* 使用 [Class.getResourceAsStream] 读取资源文件
|
||||
*/
|
||||
fun Class<*>.asResourceContainer(): ResourceContainer = ClassAsResourceContainer(this)
|
@ -18,7 +18,7 @@ import kotlin.test.assertSame
|
||||
|
||||
internal class SettingTest {
|
||||
|
||||
class MySetting : Setting() {
|
||||
class MySetting : AbstractSetting() {
|
||||
var int by value(1)
|
||||
val map by value<MutableMap<String, String>>()
|
||||
val map2 by value<MutableMap<String, MutableMap<String, String>>>()
|
||||
|
Loading…
Reference in New Issue
Block a user