aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/dev/isxander/yacl3
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/dev/isxander/yacl3')
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/API.kt183
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Controllers.kt119
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt59
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Impl.kt298
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt103
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt283
6 files changed, 610 insertions, 435 deletions
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
index 87778e5..cd2c483 100644
--- a/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
@@ -1,63 +1,154 @@
package dev.isxander.yacl3.dsl
import dev.isxander.yacl3.api.*
+import net.minecraft.network.chat.CommonComponents
import net.minecraft.network.chat.Component
+import java.util.concurrent.CompletableFuture
+import kotlin.properties.ReadOnlyProperty
-interface YACLDsl {
- val namespaceKey: String
+interface Buildable<T> {
+ val built: CompletableFuture<T>
- val categories: YACLDslReference
+ fun build(): T
+}
- fun title(component: Component)
- fun title(block: () -> Component)
+fun <T> CompletableFuture<T>.onReady(block: (T) -> Unit) =
+ this.whenComplete { result, _ -> result?.let(block) }
- fun category(id: String, block: CategoryDsl.() -> Unit): ConfigCategory
+operator fun <T> CompletableFuture<out ParentRegistrar<*, *, T>>.get(id: String): CompletableFuture<T> =
+ thenCompose { it[id] }
- fun save(block: () -> Unit)
+typealias FutureOption<T> = CompletableFuture<Option<T>>
+
+fun <T> CompletableFuture<OptionRegistrar>.futureRef(id: String): FutureOption<T> =
+ thenCompose { it.futureRef(id) }
+
+fun YetAnotherConfigLib(id: String, block: RootDsl.() -> Unit) =
+ RootDslImpl(id).apply(block).build()
+
+interface ParentRegistrar<T, DSL, INNER> {
+ fun register(id: String, registrant: T): T
+
+ fun register(id: String, block: DSL.() -> Unit): T
+
+ /** Registers a registrant via delegation - if id is not provided, the delegated property name is used */
+ fun registering(id: String? = null, block: DSL.() -> Unit): RegisterableActionDelegateProvider<DSL, T>
+
+ /** Creates a delegated future reference to a registrant that may or may not exist yet */
+ val futureRef: ReadOnlyProperty<Any?, CompletableFuture<T>>
+
+ /** Creates a future reference to a registrant that may or may not exist yet */
+ fun futureRef(id: String): CompletableFuture<T>
+
+ /** Gets a registrant with the id, if it exists */
+ fun ref(id: String): T?
+
+ /** Creates a delegated property that returns a registrant with a matching id, or null if it does not exist at the time of calling */
+ val ref: ReadOnlyProperty<Any?, T?>
+
+ operator fun get(id: String): CompletableFuture<INNER>
}
-interface OptionAddableDsl {
- fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T>
+interface OptionRegistrar {
+ /** Registers an option that has already been built. */
+ fun <T, OPT : Option<T>> register(id: String, option: OPT): OPT
+
+ /** Registers a regular option */
+ fun <T> register(id: String, block: OptionDsl<T>.() -> Unit): Option<T>
+
+ /** Registers a regular option via delegation */
+ fun <T> registering(id: String? = null, block: OptionDsl<T>.() -> Unit): RegisterableActionDelegateProvider<OptionDsl<T>, Option<T>>
+
+ fun <T> futureRef(id: String): CompletableFuture<Option<T>>
+ fun <T> futureRef(): RegisterableDelegateProvider<CompletableFuture<Option<T>>>
+
+ fun <T> ref(id: String? = null): ReadOnlyProperty<Any?, Option<T>?>
+
+
+ fun registerLabel(id: String): LabelOption
+ val registeringLabel: RegisterableDelegateProvider<LabelOption>
+
+ fun registerLabel(id: String, text: Component): LabelOption
+
+ fun registerLabel(id: String, builder: TextLineBuilderDsl.() -> Unit): LabelOption
+
+ fun registerButton(id: String, block: ButtonOptionDsl.() -> Unit): ButtonOption
+ fun registeringButton(id: String? = null, block: ButtonOptionDsl.() -> Unit): RegisterableActionDelegateProvider<ButtonOptionDsl, ButtonOption>
}
-interface CategoryDsl : OptionAddableDsl {
- val groups: CategoryDslReference
- val options: GroupDslReference
+typealias CategoryRegistrar = ParentRegistrar<ConfigCategory, CategoryDsl, GroupRegistrar>
+typealias GroupRegistrar = ParentRegistrar<OptionGroup, GroupDsl, OptionRegistrar>
+
+interface RootDsl {
+ val rootKey: String
+ val rootId: String
+ val thisRoot: CompletableFuture<YetAnotherConfigLib>
- fun group(id: String, block: GroupDsl.() -> Unit): OptionGroup
+ val categories: CategoryRegistrar
+
+ fun title(component: Component)
+ fun title(block: () -> Component)
+
+ fun screenInit(block: () -> Unit)
+ fun save(block: () -> Unit)
+}
+
+interface CategoryDsl : Buildable<ConfigCategory> {
+ val categoryKey: String
+ val categoryId: String
+ val thisCategory: CompletableFuture<ConfigCategory>
+
+ val groups: GroupRegistrar
+ val rootOptions: OptionRegistrar
fun name(component: Component)
fun name(block: () -> Component)
fun tooltip(vararg component: Component)
- fun tooltipBuilder(block: TooltipBuilderDsl.() -> Unit)
- fun useDefaultTooltip(lines: Int = 1)
+ fun tooltip(block: TextLineBuilderDsl.() -> Unit)
}
-interface GroupDsl : OptionAddableDsl {
- val options: GroupDslReference
+interface GroupDsl : Buildable<OptionGroup> {
+ val groupKey: String
+ val groupId: String
+ val thisGroup: CompletableFuture<OptionGroup>
+
+ val options: OptionRegistrar
fun name(component: Component)
fun name(block: () -> Component)
- fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit)
fun description(description: OptionDescription)
- fun useDefaultDescription(lines: Int = 1)
+ fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit)
+ fun OptionDescription.Builder.addDefaultText(lines: Int? = null) =
+ addDefaultText("$groupKey.description", lines)
+}
+
+interface OptionDsl<T> : Option.Builder<T>, Buildable<Option<T>> {
+ val optionKey: String
+ val optionId: String
+ val thisOption: CompletableFuture<Option<T>>
+
+ fun OptionDescription.Builder.addDefaultText(lines: Int? = null) =
+ addDefaultText("$optionKey.description", lines)
}
-interface OptionDsl<T> : Option.Builder<T> {
- val option: FutureValue<Option<T>>
+interface ButtonOptionDsl : ButtonOption.Builder, Buildable<ButtonOption> {
+ val optionKey: String
+ val optionId: String
+ val thisOption: CompletableFuture<ButtonOption>
- fun OptionDescription.Builder.addDefaultDescription(lines: Int? = null)
+ fun OptionDescription.Builder.addDefaultText(lines: Int? = null) =
+ addDefaultText("$optionKey.description", lines)
}
-interface TooltipBuilderDsl {
+interface TextLineBuilderDsl {
fun text(component: Component)
fun text(block: () -> Component)
operator fun Component.unaryPlus()
- class Delegate(private val tooltipFunction: (Component) -> Unit) : TooltipBuilderDsl {
+ class Delegate(private val tooltipFunction: (Component) -> Unit) : TextLineBuilderDsl {
override fun text(component: Component) {
tooltipFunction(component)
}
@@ -70,36 +161,18 @@ interface TooltipBuilderDsl {
text(this)
}
}
-}
-
-interface YACLDslReference : Reference<CategoryDslReference> {
- fun get(): YetAnotherConfigLib?
-
- val isBuilt: Boolean
-
- fun registering(block: CategoryDsl.() -> Unit): RegisterableDelegateProvider<CategoryDsl, ConfigCategory>
-}
-interface CategoryDslReference : Reference<GroupDslReference> {
- fun get(): ConfigCategory?
-
- val root: GroupDslReference
-
- val isBuilt: Boolean
-
- fun registering(block: GroupDsl.() -> Unit): RegisterableDelegateProvider<GroupDsl, OptionGroup>
-}
-
-interface GroupDslReference {
- fun get(): OptionGroup?
-
- operator fun <T> get(id: String): FutureValue<Option<T>>
-
- val isBuilt: Boolean
-
- fun <T : Any> registering(block: OptionDsl<T>.() -> Unit): RegisterableDelegateProvider<OptionDsl<T>, Option<T>>
+ companion object {
+ fun createText(block: TextLineBuilderDsl.() -> Unit): Component {
+ val text = Component.empty()
+ var first = true
+ val builder = Delegate {
+ if (!first) text.append(CommonComponents.NEW_LINE)
+ text.append(it)
+ first = false
+ }
+ block(builder)
+ return text
+ }
+ }
}
-
-
-
-
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Controllers.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Controllers.kt
new file mode 100644
index 0000000..12bb9e0
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Controllers.kt
@@ -0,0 +1,119 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.Option
+import dev.isxander.yacl3.api.controller.*
+import net.minecraft.world.item.Item
+import java.awt.Color
+
+typealias ControllerBuilderFactory<T> = (Option<T>) -> ControllerBuilder<T>
+
+fun tickBox(): ControllerBuilderFactory<Boolean> = { option ->
+ TickBoxControllerBuilder.create(option)
+}
+
+fun textSwitch(formatter: ValueFormatter<Boolean>? = null): ControllerBuilderFactory<Boolean> = { option ->
+ BooleanControllerBuilder.create(option).apply {
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun slider(range: IntRange, step: Int = 1, formatter: ValueFormatter<Int>? = null): ControllerBuilderFactory<Int> = { option ->
+ IntegerSliderControllerBuilder.create(option).apply {
+ range(range.first, range.last)
+ step(step)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun slider(range: LongRange, step: Long = 1, formatter: ValueFormatter<Long>? = null): ControllerBuilderFactory<Long> = { option ->
+ LongSliderControllerBuilder.create(option).apply {
+ range(range.first, range.last)
+ step(step)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun slider(range: ClosedRange<Float>, step: Float = 1f, formatter: ValueFormatter<Float>? = null): ControllerBuilderFactory<Float> = { option ->
+ FloatSliderControllerBuilder.create(option).apply {
+ range(range.start, range.endInclusive)
+ step(step)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun slider(range: ClosedRange<Double>, step: Double = 1.0, formatter: ValueFormatter<Double>? = null): ControllerBuilderFactory<Double> = { option ->
+ DoubleSliderControllerBuilder.create(option).apply {
+ range(range.start, range.endInclusive)
+ step(step)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun stringField(): ControllerBuilderFactory<String> = { option ->
+ StringControllerBuilder.create(option)
+}
+
+fun numberField(min: Int? = null, max: Int? = null, formatter: ValueFormatter<Int>? = null): ControllerBuilderFactory<Int> = { option ->
+ IntegerFieldControllerBuilder.create(option).apply {
+ min?.let { min(it) }
+ max?.let { max(it) }
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun numberField(min: Long? = null, max: Long? = null, formatter: ValueFormatter<Long>? = null): ControllerBuilderFactory<Long> = { option ->
+ LongFieldControllerBuilder.create(option).apply {
+ min?.let { min(it) }
+ max?.let { max(it) }
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun numberField(min: Float? = null, max: Float? = null, formatter: ValueFormatter<Float>? = null): ControllerBuilderFactory<Float> = { option ->
+ FloatFieldControllerBuilder.create(option).apply {
+ min?.let { min(it) }
+ max?.let { max(it) }
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun numberField(min: Double? = null, max: Double? = null, formatter: ValueFormatter<Double>? = null): ControllerBuilderFactory<Double> = { option ->
+ DoubleFieldControllerBuilder.create(option).apply {
+ min?.let { min(it) }
+ max?.let { max(it) }
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun colorPicker(allowAlpha: Boolean = false): ControllerBuilderFactory<Color> = { option ->
+ ColorControllerBuilder.create(option).apply {
+ allowAlpha(allowAlpha)
+ }
+}
+
+fun <T> cyclingList(values: Iterable<T>, formatter: ValueFormatter<T>? = null): ControllerBuilderFactory<T> = { option ->
+ CyclingListControllerBuilder.create(option).apply {
+ values(values)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun <T : Enum<T>> enumSwitch(enumClass: Class<T>, formatter: ValueFormatter<T>? = null): ControllerBuilderFactory<T> = { option ->
+ EnumControllerBuilder.create(option).apply {
+ enumClass(enumClass)
+ formatter?.let { formatValue(it) }
+ }
+}
+
+inline fun <reified T : Enum<T>> enumSwitch(formatter: ValueFormatter<T>? = null): ControllerBuilderFactory<T> =
+ enumSwitch(T::class.java, formatter)
+
+fun <T : Enum<T>> enumDropdown(formatter: ValueFormatter<T>? = null): ControllerBuilderFactory<T> = { option ->
+ EnumDropdownControllerBuilder.create(option).apply {
+ formatter?.let { formatValue(it) }
+ }
+}
+
+fun minecraftItem(): ControllerBuilderFactory<Item> = { option ->
+ ItemControllerBuilder.create(option)
+}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
index 4b93f5f..7349850 100644
--- a/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
@@ -1,9 +1,12 @@
package dev.isxander.yacl3.dsl
+import dev.isxander.yacl3.api.Binding
+import dev.isxander.yacl3.api.ButtonOption
import dev.isxander.yacl3.api.Option
import dev.isxander.yacl3.api.OptionDescription
import dev.isxander.yacl3.api.OptionGroup
import dev.isxander.yacl3.api.controller.ControllerBuilder
+import net.minecraft.locale.Language
import net.minecraft.network.chat.Component
import kotlin.reflect.KMutableProperty0
@@ -11,14 +14,62 @@ fun <T : Any> Option.Builder<T>.binding(property: KMutableProperty0<T>, default:
binding(default, { property.get() }, { property.set(it) })
}
-fun <T : Any> Option.Builder<T>.descriptionBuilder(block: OptionDescription.Builder.(T) -> Unit) {
+var <T> Option.Builder<T>.controller: ControllerBuilderFactory<T>
+ get() = throw UnsupportedOperationException()
+ set(value) {
+ controller(value)
+ }
+
+var <T> Option.Builder<T>.binding: Binding<T>
+ get() = throw UnsupportedOperationException()
+ set(value) {
+ binding(value)
+ }
+
+var Option.Builder<*>.available: Boolean
+ get() = throw UnsupportedOperationException()
+ set(value) {
+ available(value)
+ }
+
+fun <T> Option.Builder<T>.descriptionBuilderDyn(block: OptionDescription.Builder.(value: T) -> Unit) {
description { OptionDescription.createBuilder().apply { block(it) }.build() }
}
-fun Option.Builder<*>.descriptionBuilderConst(block: OptionDescription.Builder.() -> Unit) {
+fun Option.Builder<*>.descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
description(OptionDescription.createBuilder().apply(block).build())
}
+fun ButtonOption.Builder.descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
+ description(OptionDescription.createBuilder().apply(block).build())
+}
+
+fun OptionGroup.Builder.descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
+ description(OptionDescription.createBuilder().apply(block).build())
+}
+
+fun OptionDescription.Builder.addDefaultText(prefix: String, lines: Int? = null) {
+ if (lines != null) {
+ if (lines == 1) {
+ text(Component.translatable(prefix))
+ } else for (i in 1..lines) {
+ text(Component.translatable("$prefix.$i"))
+ }
+ } else {
+ // loop until we find a key that doesn't exist
+ var i = 1
+ while (i < 100) {
+ val key = "$prefix.$i"
+ if (!Language.getInstance().has(key)) {
+ break
+ }
+ text(Component.translatable(key))
+
+ i++
+ }
+ }
+}
+
fun Option.Builder<*>.available(block: () -> Boolean) {
available(block())
}
@@ -27,10 +78,6 @@ fun OptionDescription.Builder.text(block: () -> Component) {
text(block())
}
-fun OptionGroup.Builder.descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
- description(OptionDescription.createBuilder().apply(block).build())
-}
-
fun <T, B : ControllerBuilder<T>> Option.Builder<T>.controller(builder: (Option<T>) -> B, block: B.() -> Unit = {}) {
controller { builder(it).apply(block) }
}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Impl.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Impl.kt
new file mode 100644
index 0000000..ab0d1a2
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Impl.kt
@@ -0,0 +1,298 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.*
+import dev.isxander.yacl3.impl.utils.YACLConstants
+import net.minecraft.network.chat.Component
+import org.slf4j.Logger
+import java.util.concurrent.CompletableFuture
+import kotlin.properties.ReadOnlyProperty
+
+private val LOGGER: Logger = YACLConstants.LOGGER
+
+class ParentRegistrarImpl<T, DSL : Buildable<T>, INNER>(
+ private val adder: (registrant: T, id: String) -> Unit,
+ private val dslFactory: (id: String) -> DSL,
+ private val getter: (id: String) -> CompletableFuture<T>,
+ private val innerGetter: (id: String) -> CompletableFuture<INNER>,
+) : ParentRegistrar<T, DSL, INNER> {
+ override fun register(id: String, registrant: T) =
+ adder(registrant, id).let { registrant }
+
+ override fun register(id: String, block: DSL.() -> Unit): T =
+ register(id, dslFactory(id).apply(block).build())
+
+ override fun registering(id: String?, block: DSL.() -> Unit) =
+ RegisterableActionDelegateProvider(this::register, block, id)
+
+ override fun futureRef(id: String): CompletableFuture<T> = getter(id)
+
+ override val futureRef: ReadOnlyProperty<Any?, CompletableFuture<T>>
+ get() = ReadOnlyProperty { _, property -> futureRef(property.name) }
+
+ override fun ref(id: String): T? =
+ futureRef(id).getNow(null)
+
+ override val ref: ReadOnlyProperty<Any?, T?>
+ get() = ReadOnlyProperty { _, property -> ref(property.name) }
+
+ override fun get(id: String): CompletableFuture<INNER> = innerGetter(id)
+}
+
+class RootDslImpl(
+ override val rootId: String
+) : RootDsl, Buildable<YetAnotherConfigLib> {
+ override val rootKey: String = "yacl3.config.$rootId"
+
+ override val thisRoot = CompletableFuture<YetAnotherConfigLib>()
+
+ override val built = thisRoot
+ private val builder = YetAnotherConfigLib.createBuilder()
+
+ private val categoryFutures = mutableMapOf<String, CompletableFuture<CategoryDsl>>()
+ private fun createFuture(id: String) = categoryFutures.computeIfAbsent(id) { CompletableFuture() }
+
+ init {
+ builder.title(Component.translatable("$rootKey.title"))
+ }
+
+ override val categories: CategoryRegistrar = ParentRegistrarImpl(
+ { category, _ -> builder.category(category) },
+ { id -> CategoryDslImpl(id, this)
+ .also { createFuture(id).complete(it) }
+ },
+ { id -> createFuture(id).thenCompose { it.built } },
+ { id -> createFuture(id).thenApply { it.groups } },
+ )
+
+ override fun title(component: Component) {
+ builder.title(component)
+ }
+
+ override fun title(block: () -> Component) = title(block())
+
+ override fun screenInit(block: () -> Unit) {
+ builder.screenInit { block() }
+ }
+
+ override fun save(block: () -> Unit) {
+ builder.save { block() }
+ }
+
+ override fun build(): YetAnotherConfigLib =
+ builder.build().also {
+ thisRoot.complete(it)
+ checkUnresolvedFutures()
+ }
+
+ private fun checkUnresolvedFutures() {
+ categoryFutures.filterValues { !it.isDone }
+ .forEach { LOGGER.error("Future category ${it.key} was referenced but was never built.") }
+ }
+}
+
+class CategoryDslImpl(
+ override val categoryId: String,
+ private val parent: RootDsl,
+) : CategoryDsl {
+ override val categoryKey = "${parent.rootKey}.category.$categoryId"
+
+ override val thisCategory = CompletableFuture<ConfigCategory>()
+
+ override val built = thisCategory
+ private val builder = ConfigCategory.createBuilder()
+
+ private val groupFutures = mutableMapOf<String, CompletableFuture<GroupDsl>>()
+ private fun createGroupFuture(id: String) = groupFutures.computeIfAbsent(id) { CompletableFuture() }
+
+ private val rootOptFutures = mutableMapOf<String, CompletableFuture<Option<*>>>()
+ private fun createRootOptFuture(id: String) = rootOptFutures.computeIfAbsent(id) { CompletableFuture() }
+
+ init {
+ builder.name(Component.translatable(categoryKey))
+ }
+
+ override val groups: GroupRegistrar = ParentRegistrarImpl(
+ { group, _ -> builder.group(group) },
+ { id -> GroupDslImpl(id, this)
+ .also { createGroupFuture(id).complete(it) }
+ },
+ { id -> createGroupFuture(id).thenCompose { it.built } },
+ { id -> createGroupFuture(id).thenApply { it.options } },
+ )
+
+ override val rootOptions: OptionRegistrar = OptionRegistrarImpl(
+ { option, id -> builder.option(option).also { createRootOptFuture(id).complete(option) } },
+ { id -> createRootOptFuture(id) },
+ "$categoryKey.root",
+ )
+
+ override fun name(component: Component) {
+ builder.name(component)
+ }
+
+ override fun name(block: () -> Component) = name(block())
+
+ override fun tooltip(block: TextLineBuilderDsl.() -> Unit) {
+ builder.tooltip(TextLineBuilderDsl.createText(block))
+ }
+
+ override fun tooltip(vararg component: Component) = tooltip {
+ component.forEach { +it }
+ }
+
+ override fun build(): ConfigCategory =
+ builder.build().also {
+ thisCategory.complete(it)
+ checkUnresolvedFutures()
+ }
+
+ private fun checkUnresolvedFutures() {
+ groupFutures.filterValues { !it.isDone }
+ .forEach { LOGGER.error("Future group $categoryId/${it.key} was referenced but was never built.") }
+ rootOptFutures.filterValues { !it.isDone }
+ .forEach { LOGGER.error("Future option $categoryId/root/${it.key} was referenced but was never built.") }
+ }
+}
+
+class GroupDslImpl(
+ override val groupId: String,
+ private val parent: CategoryDsl,
+) : GroupDsl {
+ override val groupKey = "${parent.categoryKey}.group.$groupId"
+
+ override val thisGroup = CompletableFuture<OptionGroup>()
+
+ override val built = thisGroup
+ private val builder = OptionGroup.createBuilder()
+
+ private val optionFutures = mutableMapOf<String, CompletableFuture<Option<*>>>()
+ private fun createOptionFuture(id: String) = optionFutures.computeIfAbsent(id) { CompletableFuture() }
+
+ init {
+ builder.name(Component.translatable(groupKey))
+ }
+
+ override val options: OptionRegistrar = OptionRegistrarImpl(
+ { option, id -> builder.option(option).also { createOptionFuture(id).complete(option) } },
+ { id -> createOptionFuture(id) },
+ groupKey,
+ )
+
+ override fun name(component: Component) {
+ builder.name(component)
+ }
+
+ override fun name(block: () -> Component) = name(block())
+
+ override fun description(description: OptionDescription) {
+ builder.description(description)
+ }
+
+ override fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
+ builder.description(OptionDescription.createBuilder().apply(block).build())
+ }
+
+ override fun OptionDescription.Builder.addDefaultText(lines: Int?) {
+ addDefaultText("$groupKey.description", lines)
+ }
+
+ override fun build(): OptionGroup =
+ builder.build().also {
+ thisGroup.complete(it)
+ checkUnresolvedFutures()
+ }
+
+ private fun checkUnresolvedFutures() {
+ optionFutures.filterValues { !it.isDone }
+ .forEach { LOGGER.error("Future option ${parent.categoryId}/$groupId/${it.key} was referenced but was never built.") }
+ }
+}
+
+class OptionRegistrarImpl(
+ private val adder: (registrant: Option<*>, id: String) -> Unit,
+ private val getter: (id: String) -> CompletableFuture<Option<*>>,
+ private val groupKey: String,
+) : OptionRegistrar {
+ override fun <T, OPT : Option<T>> register(id: String, option: OPT): OPT =
+ adder(option, id).let { option }
+
+ override fun <T> register(id: String, block: OptionDsl<T>.() -> Unit): Option<T> =
+ register(id, OptionDslImpl<T>(id, groupKey).apply(block).build())
+
+ override fun <T> registering(
+ id: String?,
+ block: OptionDsl<T>.() -> Unit
+ ) = RegisterableActionDelegateProvider(this::register, block, id)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> futureRef(id: String): CompletableFuture<Option<T>> =
+ getter(id) as CompletableFuture<Option<T>>
+
+ override fun <T> futureRef() =
+ RegisterableDelegateProvider({ this.futureRef<T>(it) }, null)
+
+ override fun <T> ref(id: String?) = ReadOnlyProperty<Any?, Option<T>?> { _, property ->
+ futureRef<T>(id ?: property.name).getNow(null)
+ }
+
+ override fun registerLabel(id: String): LabelOption =
+ register(id, LabelOption.create(Component.translatable("$groupKey.label.$id")))
+
+ override val registeringLabel = RegisterableDelegateProvider(this::registerLabel, null)
+
+ override fun registerLabel(id: String, text: Component): LabelOption =
+ register(id, LabelOption.create(text))
+
+ override fun registerLabel(id: String, builder: TextLineBuilderDsl.() -> Unit): LabelOption =
+ registerLabel(id, TextLineBuilderDsl.createText(builder))
+
+ override fun registerButton(id: String, block: ButtonOptionDsl.() -> Unit): ButtonOption =
+ register(id, ButtonOptionDslImpl(id, groupKey).apply(block).build())
+
+ override fun registeringButton(
+ id: String?,
+ block: ButtonOptionDsl.() -> Unit
+ ) = RegisterableActionDelegateProvider(this::registerButton, block, id)
+}
+
+class OptionDslImpl<T>(
+ override val optionId: String,
+ groupKey: String,
+ private val builder: Option.Builder<T> = Option.createBuilder(),
+) : OptionDsl<T>, Option.Builder<T> by builder {
+ override val optionKey = "$groupKey.option.$optionId"
+
+ override val thisOption = CompletableFuture<Option<T>>()
+ override val built = thisOption
+
+ init {
+ builder.name(Component.translatable(optionKey))
+ }
+
+ override fun OptionDescription.Builder.addDefaultText(lines: Int?) =
+ addDefaultText(prefix = "$optionKey.description", lines = lines)
+
+ override fun build(): Option<T> =
+ builder.build().also { thisOption.complete(it) }
+}
+
+class ButtonOptionDslImpl(
+ override val optionId: String,
+ groupKey: String,
+ private val builder: ButtonOption.Builder = ButtonOption.createBuilder(),
+) : ButtonOptionDsl, ButtonOption.Builder by builder {
+ override val optionKey = "$groupKey.option.$optionId"
+
+ override val thisOption = CompletableFuture<ButtonOption>()
+ override val built = thisOption
+
+ init {
+ builder.name(Component.translatable(optionKey))
+ }
+
+ override fun OptionDescription.Builder.addDefaultText(lines: Int?) =
+ addDefaultText(prefix = "$optionKey.description", lines = lines)
+
+ override fun build(): ButtonOption =
+ builder.build().also { thisOption.complete(it) }
+}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
index 819365c..87337aa 100644
--- a/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
@@ -1,110 +1,31 @@
package dev.isxander.yacl3.dsl
-import dev.isxander.yacl3.api.Option
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
-interface FutureValue<T> {
- fun onReady(block: (T) -> Unit)
- fun <R> map(block: (T) -> R): FutureValue<R>
- fun <R> flatMap(block: (T) -> FutureValue<R>): FutureValue<R>
- fun getOrNull(): T?
- fun getOrThrow(): T = getOrNull() ?: error("Value not ready yet!")
-
- open class Impl<T>(default: T? = null) : FutureValue<T> {
- var value: T? = default
- set(value) {
- field = value
- while (taskQueue.isNotEmpty()) {
- taskQueue.removeFirst()(value!!)
- }
- }
- private val taskQueue = ArrayDeque<(T) -> Unit>()
-
- override fun onReady(block: (T) -> Unit) {
- if (value != null) block(value!!)
- else taskQueue.add(block)
- }
-
- override fun <R> map(block: (T) -> R): FutureValue<R> {
- val future = Impl<R>()
- onReady {
- future.value = block(it)
- }
- return future
- }
-
- override fun <R> flatMap(block: (T) -> FutureValue<R>): FutureValue<R> {
- val future = Impl<R>()
- onReady {
- block(it).onReady { inner ->
- future.value = inner
- }
- }
- return future
- }
-
- override fun getOrNull(): T? = value
- }
-}
-
-interface Reference<T> : ReadOnlyProperty<Any?, FutureValue<T>> {
- operator fun get(id: String): FutureValue<T>
-
- override fun getValue(thisRef: Any?, property: KProperty<*>): FutureValue<T> {
- return get(property.name)
- }
-
- operator fun invoke(name: String? = null, block: (T) -> Unit): ReadOnlyProperty<Any?, FutureValue<T>> {
- return ReadOnlyProperty { thisRef, property ->
- val future = get(name ?: property.name)
- future.onReady(block)
- future
- }
- }
-}
-
-
-operator fun <T> FutureValue<out Reference<T>>.get(id: String): FutureValue<T> {
- val future = FutureValue.Impl<FutureValue<T>>()
- onReady {
- future.value = it[id]
- }
- return future.flatten()
-}
-
-fun FutureValue<GroupDslReference>.getOption(id: String): FutureValue<Option<*>> {
- val future = FutureValue.Impl<FutureValue<Option<*>>>()
- onReady {
- future.value = it.get<Any?>(id) as FutureValue<Option<*>>
- }
- return future.flatten()
-}
-
-
-private fun <T> FutureValue<FutureValue<T>>.flatten(): FutureValue<T> {
- val future = FutureValue.Impl<T>()
- onReady { outer ->
- outer.onReady { inner ->
- future.value = inner
- }
+class RegisterableDelegateProvider<R>(
+ private val registerFunction: (id: String) -> R,
+ private val id: String?,
+) {
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ExistingDelegateProvider<R> {
+ return ExistingDelegateProvider(registerFunction(id ?: property.name))
}
- return future
}
-class RegisterableDelegateProvider<Dsl, Return>(
+class RegisterableActionDelegateProvider<Dsl, Return>(
private val registerFunction: (String, Dsl.() -> Unit) -> Return,
- private val action: Dsl.() -> Unit
+ private val action: Dsl.() -> Unit,
+ private val name: String?
) {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ExistingDelegateProvider<Return> {
- return ExistingDelegateProvider(registerFunction(property.name, action))
+ return ExistingDelegateProvider(registerFunction(name ?: property.name, action))
}
}
class ExistingDelegateProvider<Return>(
private val delegate: Return
-) {
- operator fun getValue(thisRef: Any?, property: KProperty<*>): Return {
+) : ReadOnlyProperty<Any?, Return> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Return {
return delegate
}
}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt
deleted file mode 100644
index 8c10cfd..0000000
--- a/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt
+++ /dev/null
@@ -1,283 +0,0 @@
-package dev.isxander.yacl3.dsl
-
-import dev.isxander.yacl3.api.*
-import net.minecraft.locale.Language
-import net.minecraft.network.chat.Component
-
-fun YetAnotherConfigLib(namespace: String, block: YACLDsl.() -> Unit): YetAnotherConfigLib {
- val context = YACLDslContext(namespace)
- context.block()
- return context.build()
-}
-
-class YACLDslContext(
- private val namespace: String,
- private val builder: YetAnotherConfigLib.Builder = YetAnotherConfigLib.createBuilder()
-) : YACLDsl {
- private val categoryMap = LinkedHashMap<String, YACLDslCategoryContext>()
- private val categoryDslReferenceMap = mutableMapOf<String, FutureValue.Impl<CategoryDslReference>>()
-
- override val namespaceKey = "yacl3.config.$namespace"
-
- private var used = false
- private var built: YetAnotherConfigLib? = null
-
- private var saveFunction: () -> Unit = {}
-
- override val categories = object : YACLDslReference {
- override fun get(): YetAnotherConfigLib? = built
-
- override operator fun get(id: String): FutureValue<CategoryDslReference> =
- FutureValue.Impl(categoryMap[id]?.groups).also { categoryDslReferenceMap[id] = it }
-
- override fun registering(block: CategoryDsl.() -> Unit): RegisterableDelegateProvider<CategoryDsl, ConfigCategory> {
- return RegisterableDelegateProvider({ id, configuration -> category(id, configuration) }, block)
- }
-
- override val isBuilt: Boolean
- get() = built != null
- }
-
- init {
- title(Component.translatable("$namespaceKey.title"))
- }
-
- override fun title(component: Component) {
- builder.title(component)
- }
-
- override fun title(block: () -> Component) {
- title(block())
- }
-
- override fun category(id: String, block: CategoryDsl.() -> Unit): ConfigCategory {
- val context = YACLDslCategoryContext(id, this)
- context.block()
- categoryMap[id] = context
- categoryDslReferenceMap[id]?.value = context.groups
-
- val built = context.build()
- builder.category(built)
-
- return built
- }
-
- override fun save(block: () -> Unit) {
- val oldSaveFunction = saveFunction
- saveFunction = { // allows stacking of save functions
- oldSaveFunction()
- block()
- }
- }
-
- fun build(): YetAnotherConfigLib {
- if (used) error("Cannot use the same DSL context twice!")
- used = true
-
- builder.save(saveFunction)
-
- return builder.build().also { built = it }
- }
-}
-
-class YACLDslCategoryContext(
- private val id: String,
- private val root: YACLDslContext,
- private val builder: ConfigCategory.Builder = ConfigCategory.createBuilder(),
-) : CategoryDsl {
- private val groupMap = LinkedHashMap<String, YACLDslGroupContext>()
- private val groupDslReferenceMap = mutableMapOf<String, FutureValue.Impl<GroupDslReference>>()
- val categoryKey = "${root.namespaceKey}.$id"
-
- private var built: ConfigCategory? = null
-
- private val rootGroup: YACLDslGroupContext = YACLDslGroupContext(id, this, builder.rootGroupBuilder(), root = true)
-
- override val groups = object : CategoryDslReference {
- override fun get(): ConfigCategory? = built
-
- override fun get(id: String): FutureValue<GroupDslReference> =
- FutureValue.Impl(groupMap[id]?.options).also { groupDslReferenceMap[id] = it }
-
- override val root: GroupDslReference
- get() = rootGroup.options
-
- override fun registering(block: GroupDsl.() -> Unit): RegisterableDelegateProvider<GroupDsl, OptionGroup> {
- return RegisterableDelegateProvider({ id, configuration -> group(id, configuration) }, block)
- }
-
- override val isBuilt: Boolean
- get() = built != null
-
- }
-
- override val options = rootGroup.options
-
- init {
- builder.name(Component.translatable("$categoryKey.title"))
- }
-
- override fun name(component: Component) {
- builder.name(component)
- }
-
- override fun name(block: () -> Component) {
- name(block())
- }
-
- override fun group(id: String, block: GroupDsl.() -> Unit): OptionGroup {
- val context = YACLDslGroupContext(id, this)
- context.block()
- groupMap[id] = context
- groupDslReferenceMap[id]?.value = context.options
-
- return context.build().also {
- builder.group(it)
- }
- }
-
- override fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T> =
- rootGroup.option(id, block)
-
- override fun tooltip(vararg component: Component) {
- builder.tooltip(*component)
- }
-
- override fun tooltipBuilder(block: TooltipBuilderDsl.() -> Unit) {
- val builder = TooltipBuilderDsl.Delegate { builder.tooltip(it) }
- builder.block()
- }
-
- override fun useDefaultTooltip(lines: Int) {
- if (lines == 1) {
- builder.tooltip(Component.translatable("$categoryKey.tooltip"))
- } else for (i in 1..lines) {
- builder.tooltip(Component.translatable("$categoryKey.tooltip.$i"))
- }
- }
-
- fun build(): ConfigCategory {
- return builder.build().also { built = it }
- }
-}
-
-class YACLDslGroupContext(
- private val id: String,
- private val category: YACLDslCategoryContext,
- private val builder: OptionGroup.Builder = OptionGroup.createBuilder(),
- private val root: Boolean = false,
-) : GroupDsl {
- private val optionMap = LinkedHashMap<String, YACLDslOptionContext<*>>()
- private val optionDslReferenceMap = mutableMapOf<String, FutureValue.Impl<Option<*>>>()
- val groupKey = "${category.categoryKey}.$id"
- private var built: OptionGroup? = null
-
- override val options = object : GroupDslReference {
- override fun get(): OptionGroup? = built
-
- override fun <T> get(id: String): FutureValue<Option<T>> =
- FutureValue.Impl(optionMap[id]).flatMap { it.option as FutureValue<Option<T>> }.also { optionDslReferenceMap[id] = it as FutureValue.Impl<Option<*>> }
-
- override fun <T : Any> registering(block: OptionDsl<T>.() -> Unit): RegisterableDelegateProvider<OptionDsl<T>, Option<T>> {
- return RegisterableDelegateProvider({ id, configuration -> option(id, configuration) }, block)
- }
-
- override val isBuilt: Boolean
- get() = built != null
-
- }
-
- override fun name(component: Component) {
- builder.name(component)
- }
-
- override fun name(block: () -> Component) {
- name(block())
- }
-
- override fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit) {
- builder.description(OptionDescription.createBuilder().apply(block).build())
- }
-
- override fun description(description: OptionDescription) {
- builder.description(description)
- }
-
- init {
- if (!root) {
- builder.name(Component.translatable("$groupKey.name"))
- }
- }
-
- override fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T> {
- val context = YACLDslOptionContext<T>(id, this)
- context.block()
- optionMap[id] = context
-
- return context.build().also {
- optionDslReferenceMap[id]?.value = it
- builder.option(it)
- }
- }
-
- override fun useDefaultDescription(lines: Int) {
- descriptionBuilder {
- if (lines == 1) {
- text(Component.translatable("$groupKey.description"))
- } else for (i in 1..lines) {
- text(Component.translatable("$groupKey.description.$i"))
- }
- }
- }
-
- fun build(): OptionGroup {
- return builder.build().also { built = it }
- }
-}
-
-class YACLDslOptionContext<T : Any>(
- private val id: String,
- private val group: YACLDslGroupContext,
- private val builder: Option.Builder<T> = Option.createBuilder()
-) : Option.Builder<T> by builder, OptionDsl<T> {
- val optionKey = "${group.groupKey}.$id"
- private var built: Option<T>? = null
-
- private val taskQueue = ArrayDeque<(Option<T>) -> Unit>()
- override val option = FutureValue.Impl<Option<T>>()
-
- init {
- name(Component.translatable("$optionKey.name"))
- }
-
- override fun OptionDescription.Builder.addDefaultDescription(lines: Int?) {
- if (lines != null) {
- if (lines == 1) {
- text(Component.translatable("$optionKey.description"))
- } else for (i in 1..lines) {
- text(Component.translatable("$optionKey.description.$i"))
- }
- } else {
- // loop until we find a key that doesn't exist
- var i = 1
- while (i < 100) {
- val key = "$optionKey.description.$i"
- if (Language.getInstance().has(key)) {
- text(Component.translatable(key))
- }
-
- i++
- }
- }
- }
-
- override fun build(): Option<T> {
- return builder.build().also {
- built = it
- option.value = it
- while (taskQueue.isNotEmpty()) {
- taskQueue.removeFirst()(it)
- }
- }
- }
-}