diff options
Diffstat (limited to 'src/main/kotlin/dev')
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/API.kt | 183 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Controllers.kt | 119 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt | 59 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Impl.kt | 298 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt | 103 | ||||
-rw-r--r-- | src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt | 283 |
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) - } - } - } -} |