aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xander@isxander.dev>2024-04-14 23:19:21 +0100
committerisXander <xander@isxander.dev>2024-04-14 23:19:21 +0100
commit97bbc5a3d91ed57e55796777bbfc117ff28e2221 (patch)
tree3b9d17cb271a7676149d9d62bcbbe32bc72d4f9c
parent26aec79e10025ff3427ceb47602156ebd670b2ac (diff)
downloadYetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.tar.gz
YetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.tar.bz2
YetAnotherConfigLib-97bbc5a3d91ed57e55796777bbfc117ff28e2221.zip
Add Kotlin DSL
-rw-r--r--build.gradle.kts25
-rw-r--r--gradle.properties4
-rw-r--r--src/main/java/dev/isxander/yacl3/api/ConfigCategory.java7
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java41
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/API.kt105
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt36
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt110
-rw-r--r--src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt283
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/GuiTest.java6
-rw-r--r--src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt130
10 files changed, 737 insertions, 10 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index 98ce2a7..f6e65e5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
`java-library`
+ kotlin("jvm") version "1.9.22"
id("dev.architectury.loom") version "1.6.+"
@@ -29,10 +30,6 @@ base {
archivesName.set(property("modName").toString())
}
-java.toolchain {
- //languageVersion.set(JavaLanguageVersion.of(17))
-}
-
stonecutter.expression {
when (it) {
"controlify" -> isPropDefined("deps.controlify")
@@ -85,11 +82,15 @@ repositories {
maven("https://maven.isxander.dev/snapshots")
maven("https://maven.quiltmc.org/repository/release")
maven("https://oss.sonatype.org/content/repositories/snapshots/")
- maven("https://api.modrinth.com/maven") {
- content {
- includeGroup("maven.modrinth")
- }
+ exclusiveContent {
+ forRepository { maven("https://api.modrinth.com/maven") }
+ filter { includeGroup("maven.modrinth") }
+ }
+ exclusiveContent {
+ forRepository { maven("https://thedarkcolour.github.io/KotlinForForge/") }
+ filter { includeGroup("thedarkcolour") }
}
+
maven("https://maven.neoforged.net/releases/")
}
@@ -115,13 +116,19 @@ dependencies {
modImplementation(fabricApi.module(it, fapiVersion))
}
modRuntimeOnly("net.fabricmc.fabric-api:fabric-api:$fapiVersion")
+
+ modImplementation("net.fabricmc:fabric-language-kotlin:${findProperty("deps.fabricLangKotlin")}")
}
if (isNeoforge) {
"neoForge"("net.neoforged:neoforge:${findProperty("deps.neoforge")}")
+
+ modImplementation("thedarkcolour:kotlinforforge-neoforge:${findProperty("deps.kotlinForForge")}")
}
if (isForge) {
"forge"("net.minecraftforge:forge:${findProperty("deps.forge")}")
+ modImplementation("thedarkcolour:kotlinforforge:${findProperty("deps.kotlinForForge")}")
+
// enable when it's needed
// val mixinExtras = findProperty("deps.mixinExtras")
// compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:$mixinExtras")!!)
@@ -175,7 +182,7 @@ tasks {
filesMatching("META-INF/mods.toml") { expand(props) }
}
- register("releaseMod") {
+ val releaseMod by registering {
group = "mod"
dependsOn("publishMods")
diff --git a/gradle.properties b/gradle.properties
index c9ee619..07acb4e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,5 +12,7 @@ deps.fabricLoader=0.15.9
deps.imageio=3.10.0
deps.quiltParsers=0.2.1
deps.mixinExtras=0.3.5
+deps.fabricLangKotlin=1.10.19+kotlin.1.9.23
+deps.kotlinForForge=4.10.0
-fabric.loom.multiProjectOptimisation=true \ No newline at end of file
+fabric.loom.multiProjectOptimisation=true
diff --git a/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java b/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java
index b3d68fc..41f3ca9 100644
--- a/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java
+++ b/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java
@@ -125,6 +125,13 @@ public interface ConfigCategory {
Builder groups(@NotNull Collection<OptionGroup> groups);
/**
+ * Fetches the builder for the root group of the category.
+ * This is the group that has no header and options are added through {@link Builder#option(Option)}.
+ * In its default implementation, this builder is severely limited and a lot of methods are unsupported.
+ */
+ OptionGroup.Builder rootGroupBuilder();
+
+ /**
* Sets the tooltip to be used by the category.
* Can be invoked twice to append more lines.
* No need to wrap the text yourself, the gui does this itself.
diff --git a/src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java b/src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java
index 400abf6..c73d647 100644
--- a/src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java
@@ -46,6 +46,8 @@ public final class ConfigCategoryImpl implements ConfigCategory {
private Component name;
private final List<Option<?>> rootOptions = new ArrayList<>();
+ private final RootGroupBuilder rootGroupBuilder = new RootGroupBuilder();
+
private final List<OptionGroup> groups = new ArrayList<>();
private final List<Component> tooltipLines = new ArrayList<>();
@@ -107,6 +109,11 @@ public final class ConfigCategoryImpl implements ConfigCategory {
}
@Override
+ public OptionGroup.Builder rootGroupBuilder() {
+ return rootGroupBuilder;
+ }
+
+ @Override
public ConfigCategory build() {
Validate.notNull(name, "`name` must not be null to build `ConfigCategory`");
@@ -130,5 +137,39 @@ public final class ConfigCategoryImpl implements ConfigCategory {
return new ConfigCategoryImpl(name, ImmutableList.copyOf(combinedGroups), concatenatedTooltip);
}
+
+ private class RootGroupBuilder implements OptionGroup.Builder {
+ @Override
+ public OptionGroup.Builder name(@NotNull Component name) {
+ throw new UnsupportedOperationException("Cannot set name of root group!");
+ }
+
+ @Override
+ public OptionGroup.Builder description(@NotNull OptionDescription description) {
+ throw new UnsupportedOperationException("Cannot set name of root group!");
+ }
+
+ @Override
+ public OptionGroup.Builder option(@NotNull Option<?> option) {
+ ConfigCategoryImpl.BuilderImpl.this.option(option);
+ return this;
+ }
+
+ @Override
+ public OptionGroup.Builder options(@NotNull Collection<? extends Option<?>> options) {
+ ConfigCategoryImpl.BuilderImpl.this.options(options);
+ return this;
+ }
+
+ @Override
+ public OptionGroup.Builder collapsed(boolean collapsible) {
+ throw new UnsupportedOperationException("Cannot set collapsible of root group!");
+ }
+
+ @Override
+ public OptionGroup build() {
+ throw new UnsupportedOperationException("Cannot build root group!");
+ }
+ }
}
}
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
new file mode 100644
index 0000000..87778e5
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/API.kt
@@ -0,0 +1,105 @@
+package dev.isxander.yacl3.dsl
+
+import dev.isxander.yacl3.api.*
+import net.minecraft.network.chat.Component
+
+interface YACLDsl {
+ val namespaceKey: String
+
+ val categories: YACLDslReference
+
+ fun title(component: Component)
+ fun title(block: () -> Component)
+
+ fun category(id: String, block: CategoryDsl.() -> Unit): ConfigCategory
+
+ fun save(block: () -> Unit)
+}
+
+interface OptionAddableDsl {
+ fun <T : Any> option(id: String, block: OptionDsl<T>.() -> Unit): Option<T>
+}
+
+interface CategoryDsl : OptionAddableDsl {
+ val groups: CategoryDslReference
+ val options: GroupDslReference
+
+ fun group(id: String, block: GroupDsl.() -> Unit): OptionGroup
+
+ fun name(component: Component)
+ fun name(block: () -> Component)
+
+ fun tooltip(vararg component: Component)
+ fun tooltipBuilder(block: TooltipBuilderDsl.() -> Unit)
+ fun useDefaultTooltip(lines: Int = 1)
+}
+
+interface GroupDsl : OptionAddableDsl {
+ val options: GroupDslReference
+
+ fun name(component: Component)
+ fun name(block: () -> Component)
+
+ fun descriptionBuilder(block: OptionDescription.Builder.() -> Unit)
+ fun description(description: OptionDescription)
+ fun useDefaultDescription(lines: Int = 1)
+}
+
+interface OptionDsl<T> : Option.Builder<T> {
+ val option: FutureValue<Option<T>>
+
+ fun OptionDescription.Builder.addDefaultDescription(lines: Int? = null)
+}
+
+interface TooltipBuilderDsl {
+ fun text(component: Component)
+ fun text(block: () -> Component)
+
+ operator fun Component.unaryPlus()
+
+ class Delegate(private val tooltipFunction: (Component) -> Unit) : TooltipBuilderDsl {
+ override fun text(component: Component) {
+ tooltipFunction(component)
+ }
+
+ override fun text(block: () -> Component) {
+ text(block())
+ }
+
+ override fun Component.unaryPlus() {
+ 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>>
+}
+
+
+
+
diff --git a/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
new file mode 100644
index 0000000..4b93f5f
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Extensions.kt
@@ -0,0 +1,36 @@
+package dev.isxander.yacl3.dsl
+
+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.network.chat.Component
+import kotlin.reflect.KMutableProperty0
+
+fun <T : Any> Option.Builder<T>.binding(property: KMutableProperty0<T>, default: T) {
+ binding(default, { property.get() }, { property.set(it) })
+}
+
+fun <T : Any> Option.Builder<T>.descriptionBuilder(block: OptionDescription.Builder.(T) -> Unit) {
+ description { OptionDescription.createBuilder().apply { block(it) }.build() }
+}
+
+fun Option.Builder<*>.descriptionBuilderConst(block: OptionDescription.Builder.() -> Unit) {
+ description(OptionDescription.createBuilder().apply(block).build())
+}
+
+fun Option.Builder<*>.available(block: () -> Boolean) {
+ available(block())
+}
+
+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/Util.kt b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
new file mode 100644
index 0000000..819365c
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/Util.kt
@@ -0,0 +1,110 @@
+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
+ }
+ }
+ return future
+}
+
+class RegisterableDelegateProvider<Dsl, Return>(
+ private val registerFunction: (String, Dsl.() -> Unit) -> Return,
+ private val action: Dsl.() -> Unit
+) {
+ operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ExistingDelegateProvider<Return> {
+ return ExistingDelegateProvider(registerFunction(property.name, action))
+ }
+}
+
+class ExistingDelegateProvider<Return>(
+ private val delegate: Return
+) {
+ operator 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
new file mode 100644
index 0000000..8c10cfd
--- /dev/null
+++ b/src/main/kotlin/dev/isxander/yacl3/dsl/YetAnotherConfigLibDsl.kt
@@ -0,0 +1,283 @@
+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)
+ }
+ }
+ }
+}
diff --git a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
index 3ddfce6..cdc1285 100644
--- a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
+++ b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
@@ -49,6 +49,12 @@ public class GuiTest {
Minecraft.getInstance().setScreen(AutogenConfigTest.INSTANCE.generateGui().generateScreen(screen));
})
.build())
+ .option(ButtonOption.createBuilder()
+ .name(Component.literal("Kotlin DSL Test"))
+ .action((screen, opt) -> {
+ Minecraft.getInstance().setScreen(DslTestKt.kotlinDslGui(screen));
+ })
+ .build())
.group(OptionGroup.createBuilder()
.name(Component.literal("Wiki"))
.option(ButtonOption.createBuilder()
diff --git a/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
new file mode 100644
index 0000000..2efd9a4
--- /dev/null
+++ b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
@@ -0,0 +1,130 @@
+package dev.isxander.yacl3.test
+
+import dev.isxander.yacl3.api.OptionFlag
+import dev.isxander.yacl3.api.controller.BooleanControllerBuilder
+import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder
+import dev.isxander.yacl3.dsl.*
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+
+object Foo {
+ var bar = true
+ var baz = 0
+}
+
+fun kotlinDslGui(parent: Screen?) = YetAnotherConfigLib("namespace") {
+ // default title with translation key:
+ // `yacl3.config.namespace.title`
+ /* NO CODE REQUIRED */
+
+ // or set the title
+ title(Component.literal("A cool title"))
+
+
+ // usual save function
+ save {
+ // run your save function!
+ }
+
+ // get access to an option from the very root of the dsl!
+ categories["testCategory"]["testGroup"].getOption("testOption").onReady {
+ // do something with it
+ }
+
+ val testCategory by categories.registering {
+ // default name with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.name`
+ /* NO CODE REQUIRED */
+
+ // or set the name
+ name { Component.literal("A cool category") }
+
+ // custom tooltip
+ tooltipBuilder {
+ // add a line like this
+ +Component.translatable("somecustomkey")
+
+ // or like this
+ text(Component.translatable("somecustomkey"))
+
+ // or like this
+ text { Component.translatable("somecustomkey") }
+ }
+
+ // you can declare things with strings
+ group("testGroup") {
+ // default name with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.name`
+ /* NO CODE REQUIRED */
+
+ // or set the name
+ name { Component.literal("A cool group") }
+
+
+ // custom description builder:
+ descriptionBuilder {
+ // blah blah blah
+ }
+
+ // default description with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.description.1-5`
+ // not compatible with custom description builder
+ useDefaultDescription(lines = 5)
+
+ // you can define opts/groups/categories using this delegate syntax
+ val testOption by options.registering { // type is automatically inferred from binding
+ // default name with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.testOption.name`
+ /* NO CODE REQUIRED */
+
+ // custom description builder:
+ descriptionBuilder { value -> // changes the desc based on the current value
+ // description with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.testOption.description.1-5`
+ addDefaultDescription(lines = 5)
+
+ text { Component.translatable("somecustomkey") }
+ webpImage(ResourceLocation("namespace", "image.png"))
+ }
+
+ // KProperties are cool!
+ binding(Foo::bar, Foo.bar)
+
+ // you can access other options like this!
+ // `options` field is from the enclosing group dsl
+ listener { opt, newVal ->
+ options.get<Int>("otherTestOption").onReady { it.setAvailable(newVal) }
+ }
+
+ // or even get an access to them before creation
+ options.get<Int>("otherTestOption").onReady {
+ // do something with it
+ }
+
+ // you can set available with a block
+ available { true }
+
+ // regular controller stuff
+ // this will be DSLed at some point
+ controller(BooleanControllerBuilder::create) {
+ // blah blah blah
+ }
+
+ // flags as usual
+ flag(OptionFlag.ASSET_RELOAD)
+ }
+
+ val otherTestOption by options.registering { // type is automatically inferred from binding
+ controller(IntegerSliderControllerBuilder::create) {
+ range(0, 100)
+ step(5)
+ }
+
+ binding(Foo::baz, Foo.baz)
+
+ // blah blah blah other stuff
+ }
+ }
+ }
+}.generateScreen(parent)