aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/dev/isxander/yacl3/api/Binding.java5
-rw-r--r--src/main/java/dev/isxander/yacl3/api/OptionDescription.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java20
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java10
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/AbstractConfigEntry.java48
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/AbstractReadonlyConfigEntry.java37
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/ChildConfigEntryImpl.java49
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/CodecConfig.java77
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/CodecConfigEntryImpl.java42
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/ConfigEntry.java38
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/EntryAddable.java11
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/JsonFileCodecConfig.java90
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/KotlinExts.kt53
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v3/ReadonlyConfigEntry.java26
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java4
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java26
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/YACLScreen.java4
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java7
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java22
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java4
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java12
-rw-r--r--src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java4
-rw-r--r--src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java8
-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
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java53
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java2
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/GuiTest.java2
-rw-r--r--src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt177
33 files changed, 1313 insertions, 580 deletions
diff --git a/src/main/java/dev/isxander/yacl3/api/Binding.java b/src/main/java/dev/isxander/yacl3/api/Binding.java
index f41b78b..61c59a2 100644
--- a/src/main/java/dev/isxander/yacl3/api/Binding.java
+++ b/src/main/java/dev/isxander/yacl3/api/Binding.java
@@ -6,6 +6,7 @@ import net.minecraft.client.OptionInstance;
import org.apache.commons.lang3.Validate;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Supplier;
/**
@@ -19,6 +20,10 @@ public interface Binding<T> {
T defaultValue();
+ default <U> Binding<U> xmap(Function<T, U> to, Function<U, T> from) {
+ return Binding.generic(to.apply(this.defaultValue()), () -> to.apply(this.getValue()), v -> this.setValue(from.apply(v)));
+ }
+
/**
* Creates a generic binding.
*
diff --git a/src/main/java/dev/isxander/yacl3/api/OptionDescription.java b/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
index 7336379..fce7e2f 100644
--- a/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
+++ b/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
@@ -128,7 +128,7 @@ public interface OptionDescription {
* <p>
* However, <strong>THIS IS NOT API SAFE!</strong> As part of the gui package, things
* may change that could break compatibility with future versions of YACL.
- * A helpful utility (that is also not API safe) is {@link ImageRenderer#getOrMakeAsync(ResourceLocation, Supplier)}
+ * A helpful utility (that is also not API safe) is {@link dev.isxander.yacl3.gui.image.ImageRendererManager#registerOrGetImage(ResourceLocation, Supplier)}
* which will cache the image renderer for the whole game lifecycle and construct it asynchronously to the render thread.
* @param image the image renderer to display
* @return this builder
@@ -136,6 +136,21 @@ public interface OptionDescription {
Builder customImage(CompletableFuture<Optional<ImageRenderer>> image);
/**
+ * Sets a custom image renderer to display with the description.
+ * This is useful for rendering other abstract things relevant to your mod.
+ * <p>
+ * However, <strong>THIS IS NOT API SAFE!</strong> As part of the gui package, things
+ * may change that could break compatibility with future versions of YACL.
+ * A helpful utility (that is also not API safe) is {@link dev.isxander.yacl3.gui.image.ImageRendererManager#registerOrGetImage(ResourceLocation, Supplier)}
+ * which will cache the image renderer for the whole game lifecycle and construct it asynchronously to the render thread.
+ * @param image the image renderer to display
+ * @return this builder
+ */
+ default Builder customImage(ImageRenderer image) {
+ return this.customImage(CompletableFuture.completedFuture(Optional.of(image)));
+ }
+
+ /**
* Sets an animated GIF image to display with the description. This is backed by a regular minecraft resource
* in your mod's /assets folder.
*
diff --git a/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java b/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
index 83a0b1c..a0f7ee4 100644
--- a/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
+++ b/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
@@ -68,12 +68,12 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
this.path = path;
this.gson = builder
.setExclusionStrategies(new ConfigExclusionStrategy())
- /*? if >1.20.4 { */
+ /*? if >1.20.4 {*/
.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY))
- /*? } elif =1.20.4 {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
- *//*? } else {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
+ /*?} elif =1.20.4 {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
+ *//*?} else {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
*//*?}*/
.registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/)
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
@@ -169,12 +169,12 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.serializeNulls()
- /*? if >1.20.4 { */
+ /*? if >1.20.4 {*/
.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY))
- /*? } elif =1.20.4 {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
- *//*? } else {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
+ /*?} elif =1.20.4 {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
+ *//*?} else {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
*//*?}*/
.registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/)
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
index 3492c55..70e0e0b 100644
--- a/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
+++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
@@ -221,12 +221,12 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.serializeNulls()
- /*? if >1.20.4 { */
+ /*? if >1.20.4 {*/
.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY))
- /*? } elif =1.20.4 {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
- *//*? } else {*//*
- .registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
+ /*?} elif =1.20.4 {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter())
+ *//*?} else {*/
+ /*.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
*//*?}*/
.registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/)
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/AbstractConfigEntry.java b/src/main/java/dev/isxander/yacl3/config/v3/AbstractConfigEntry.java
new file mode 100644
index 0000000..8092f23
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/AbstractConfigEntry.java
@@ -0,0 +1,48 @@
+package dev.isxander.yacl3.config.v3;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+@ApiStatus.Experimental
+public abstract class AbstractConfigEntry<T> extends AbstractReadonlyConfigEntry<T> implements ConfigEntry<T> {
+ private T value;
+ private final T defaultValue;
+
+ private Function<T, T> setModifier;
+
+ public AbstractConfigEntry(String fieldName, T defaultValue) {
+ super(fieldName);
+ this.value = defaultValue;
+ this.defaultValue = defaultValue;
+ this.setModifier = UnaryOperator.identity();
+ }
+
+ @Override
+ protected T innerGet() {
+ return this.value;
+ }
+
+ @Override
+ public void set(T value) {
+ this.value = this.setModifier.apply(value);
+ }
+
+ @Override
+ public T defaultValue() {
+ return this.defaultValue;
+ }
+
+ @Override
+ public ConfigEntry<T> modifyGet(UnaryOperator<T> modifier) {
+ super.modifyGet(modifier);
+ return this;
+ }
+
+ @Override
+ public ConfigEntry<T> modifySet(UnaryOperator<T> modifier) {
+ this.setModifier = this.setModifier.andThen(modifier);
+ return this;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/AbstractReadonlyConfigEntry.java b/src/main/java/dev/isxander/yacl3/config/v3/AbstractReadonlyConfigEntry.java
new file mode 100644
index 0000000..7f59853
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/AbstractReadonlyConfigEntry.java
@@ -0,0 +1,37 @@
+package dev.isxander.yacl3.config.v3;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+@ApiStatus.Experimental
+public abstract class AbstractReadonlyConfigEntry<T> implements ReadonlyConfigEntry<T> {
+ private final String fieldName;
+
+ private Function<T, T> getModifier;
+
+ public AbstractReadonlyConfigEntry(String fieldName) {
+ this.fieldName = fieldName;
+ this.getModifier = UnaryOperator.identity();
+ }
+
+ @Override
+ public String fieldName() {
+ return fieldName;
+ }
+
+ @Override
+ public T get() {
+ return this.getModifier.apply(this.innerGet());
+ }
+
+ protected abstract T innerGet();
+
+ @Override
+ public ReadonlyConfigEntry<T> modifyGet(UnaryOperator<T> modifier) {
+ this.getModifier = this.getModifier.andThen(modifier);
+ return this;
+ }
+
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/ChildConfigEntryImpl.java b/src/main/java/dev/isxander/yacl3/config/v3/ChildConfigEntryImpl.java
new file mode 100644
index 0000000..5b608de
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/ChildConfigEntryImpl.java
@@ -0,0 +1,49 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.DynamicOps;
+import com.mojang.serialization.MapCodec;
+import com.mojang.serialization.RecordBuilder;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Optional;
+
+@ApiStatus.Experimental
+public class ChildConfigEntryImpl<T extends CodecConfig<T>> extends AbstractReadonlyConfigEntry<T> {
+ private final T config;
+ private final MapCodec<T> mapCodec;
+
+ public ChildConfigEntryImpl(String fieldName, T config) {
+ super(fieldName);
+ this.config = config;
+ this.mapCodec = config.fieldOf(this.fieldName());
+ }
+
+ @Override
+ protected T innerGet() {
+ return config;
+ }
+
+ @Override
+ public <R> RecordBuilder<R> encode(DynamicOps<R> ops, RecordBuilder<R> recordBuilder) {
+ return mapCodec.encode(config, ops, recordBuilder);
+ }
+
+ @Override
+ public <R> boolean decode(R encoded, DynamicOps<R> ops) {
+ DataResult<T> result = mapCodec.decoder().parse(ops, encoded);
+
+ //? if >1.20.4 {
+ Optional<DataResult.Error<T>> error = result.error();
+ //?} else {
+ /*Optional<DataResult.PartialResult<T>> error = result.error();
+ *///?}
+ if (error.isPresent()) {
+ YACLConstants.LOGGER.error("Failed to decode entry {}: {}", this.fieldName(), error.get().message());
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/CodecConfig.java b/src/main/java/dev/isxander/yacl3/config/v3/CodecConfig.java
new file mode 100644
index 0000000..833a7ef
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/CodecConfig.java
@@ -0,0 +1,77 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.mojang.datafixers.util.Pair;
+import com.mojang.serialization.*;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@ApiStatus.Experimental
+public abstract class CodecConfig<S extends CodecConfig<S>> implements EntryAddable, Codec<S> {
+ private final List<ReadonlyConfigEntry<?>> entries = new ArrayList<>();
+
+ public CodecConfig() {
+ // cast here to throw immediately on construction
+ var ignored = (S) this;
+ }
+
+ @Override
+ public <T> ConfigEntry<T> register(String fieldName, T defaultValue, Codec<T> codec) {
+ ConfigEntry<T> entry = new CodecConfigEntryImpl<>(fieldName, defaultValue, codec);
+ entries.add(entry);
+ return entry;
+ }
+
+ @Override
+ public <T extends CodecConfig<T>> ReadonlyConfigEntry<T> register(String fieldName, T configInstance) {
+ ReadonlyConfigEntry<T> entry = new ChildConfigEntryImpl<>(fieldName, configInstance);
+ entries.add(entry);
+ return entry;
+ }
+
+ protected void onFinishedDecode(boolean successful) {
+ }
+
+ @Override
+ public <R> DataResult<R> encode(S input, DynamicOps<R> ops, R prefix) {
+ if (input != null && input != this) {
+ throw new IllegalArgumentException("`input` is ignored. It must be null or equal to `this`.");
+ }
+
+ return this.encode(ops, prefix);
+ }
+
+ @Override
+ public <R> DataResult<Pair<S, R>> decode(DynamicOps<R> ops, R input) {
+ this.decode(input, ops);
+ return DataResult.success(Pair.of((S) this, input));
+ }
+
+ public final <R> DataResult<R> encode(DynamicOps<R> ops, R prefix) {
+ RecordBuilder<R> builder = ops.mapBuilder();
+ for (ReadonlyConfigEntry<?> entry : entries) {
+ builder = entry.encode(ops, builder);
+ }
+ return builder.build(prefix);
+ }
+
+ public final <R> DataResult<R> encodeStart(DynamicOps<R> ops) {
+ return this.encode(ops, ops.empty());
+ }
+
+ /**
+ * @return true if decoding of all entries was successful
+ */
+ public final <R> boolean decode(R encoded, DynamicOps<R> ops) {
+ boolean success = true;
+
+ for (ReadonlyConfigEntry<?> entry : entries) {
+ success &= entry.decode(encoded, ops);
+ }
+
+ onFinishedDecode(success);
+
+ return success;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/CodecConfigEntryImpl.java b/src/main/java/dev/isxander/yacl3/config/v3/CodecConfigEntryImpl.java
new file mode 100644
index 0000000..855fd22
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/CodecConfigEntryImpl.java
@@ -0,0 +1,42 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.mojang.serialization.*;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Optional;
+
+@ApiStatus.Experimental
+public class CodecConfigEntryImpl<T> extends AbstractConfigEntry<T> {
+ private final MapCodec<T> mapCodec;
+
+ public CodecConfigEntryImpl(String fieldName, T defaultValue, Codec<T> codec) {
+ super(fieldName, defaultValue);
+ this.mapCodec = codec.fieldOf(this.fieldName());
+ }
+
+ @Override
+ public <R> RecordBuilder<R> encode(DynamicOps<R> ops, RecordBuilder<R> recordBuilder) {
+ return mapCodec.encode(get(), ops, recordBuilder);
+ }
+
+ @Override
+ public <R> boolean decode(R encoded, DynamicOps<R> ops) {
+ DataResult<T> result = mapCodec.decoder().parse(ops, encoded);
+
+ //? if >1.20.4 {
+ Optional<DataResult.Error<T>> error = result.error();
+ //?} else {
+ /*Optional<DataResult.PartialResult<T>> error = result.error();
+ *///?}
+ if (error.isPresent()) {
+ YACLConstants.LOGGER.error("Failed to decode entry {}: {}", this.fieldName(), error.get().message());
+ return false;
+ }
+
+ T value = result.result().orElseThrow();
+ this.set(value);
+
+ return true;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/ConfigEntry.java b/src/main/java/dev/isxander/yacl3/config/v3/ConfigEntry.java
new file mode 100644
index 0000000..c22d796
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/ConfigEntry.java
@@ -0,0 +1,38 @@
+package dev.isxander.yacl3.config.v3;
+
+import dev.isxander.yacl3.api.Binding;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.function.Consumer;
+import java.util.function.UnaryOperator;
+
+@ApiStatus.Experimental
+public interface ConfigEntry<T> extends ReadonlyConfigEntry<T> {
+
+ void set(T value);
+
+ T defaultValue();
+
+ @Override
+ ConfigEntry<T> modifyGet(UnaryOperator<T> modifier);
+
+ @Override
+ default ConfigEntry<T> onGet(Consumer<T> consumer) {
+ return this.modifyGet(v -> {
+ consumer.accept(v);
+ return v;
+ });
+ }
+
+ ConfigEntry<T> modifySet(UnaryOperator<T> modifier);
+ default ConfigEntry<T> onSet(Consumer<T> consumer) {
+ return this.modifySet(v -> {
+ consumer.accept(v);
+ return v;
+ });
+ }
+
+ default Binding<T> asBinding() {
+ return Binding.generic(this.defaultValue(), this::get, this::set);
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/EntryAddable.java b/src/main/java/dev/isxander/yacl3/config/v3/EntryAddable.java
new file mode 100644
index 0000000..9ebd12d
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/EntryAddable.java
@@ -0,0 +1,11 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.mojang.serialization.Codec;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Experimental
+public interface EntryAddable {
+ <T> ConfigEntry<T> register(String fieldName, T defaultValue, Codec<T> codec);
+
+ <T extends CodecConfig<T>> ReadonlyConfigEntry<T> register(String fieldName, T configInstance);
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/JsonFileCodecConfig.java b/src/main/java/dev/isxander/yacl3/config/v3/JsonFileCodecConfig.java
new file mode 100644
index 0000000..49a0dac
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/JsonFileCodecConfig.java
@@ -0,0 +1,90 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.google.gson.*;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.JsonOps;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+@ApiStatus.Experimental
+public abstract class JsonFileCodecConfig extends CodecConfig {
+ private final Path configPath;
+ private final Gson gson;
+
+ public JsonFileCodecConfig(Path configPath) {
+ this.configPath = configPath;
+ this.gson = createGson();
+ }
+
+ public void saveToFile() {
+ DataResult<JsonElement> jsonTreeResult = this.encodeStart(JsonOps.INSTANCE);
+ if (jsonTreeResult.error().isPresent()) {
+ onSaveError(
+ SaveError.ENCODING,
+ new IllegalStateException("Failed to encode: " + jsonTreeResult.error().get().message())
+ );
+ return;
+ }
+
+ JsonElement jsonTree = jsonTreeResult.result().orElseThrow();
+ String json = gson.toJson(jsonTree);
+
+ try {
+ Files.writeString(configPath, json, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+ } catch (IOException e) {
+ onSaveError(SaveError.WRITING, e);
+ }
+ }
+
+ public boolean loadFromFile() {
+ if (Files.notExists(configPath)) {
+ return false;
+ }
+
+ String json;
+ try {
+ json = Files.readString(configPath);
+ } catch (IOException e) {
+ onLoadError(LoadError.READING, e);
+ return false;
+ }
+
+ JsonElement jsonTree;
+ try {
+ jsonTree = JsonParser.parseString(json);
+ } catch (JsonParseException e) {
+ onLoadError(LoadError.JSON_PARSING, e);
+ return false;
+ }
+
+ return this.decode(jsonTree, JsonOps.INSTANCE);
+ }
+
+ protected Gson createGson() {
+ return new GsonBuilder().setPrettyPrinting().create();
+ }
+
+ protected void onSaveError(SaveError error, @Nullable Throwable e) {
+ throw new IllegalStateException("Error whilst " + error.name().toLowerCase(), e);
+ }
+
+ protected void onLoadError(LoadError error, @Nullable Throwable e) {
+ throw new IllegalStateException("Error whilst " + error.name().toLowerCase(), e);
+ }
+
+ protected enum SaveError {
+ WRITING,
+ ENCODING,
+ }
+
+ protected enum LoadError {
+ READING,
+ JSON_PARSING,
+ DECODING,
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/KotlinExts.kt b/src/main/java/dev/isxander/yacl3/config/v3/KotlinExts.kt
new file mode 100644
index 0000000..2c51656
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/KotlinExts.kt
@@ -0,0 +1,53 @@
+package dev.isxander.yacl3.config.v3
+
+import com.mojang.serialization.Codec
+import dev.isxander.yacl3.dsl.OptionDsl
+import dev.isxander.yacl3.dsl.OptionRegistrar
+import org.jetbrains.annotations.ApiStatus
+import kotlin.properties.PropertyDelegateProvider
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+@ApiStatus.Experimental
+fun <T> EntryAddable.register(
+ default: T,
+ codec: Codec<T>
+) = PropertyDelegateProvider<EntryAddable, ReadOnlyProperty<EntryAddable, ConfigEntry<T>>> { thisRef, property ->
+ val entry = thisRef.register(property.name, default, codec)
+ ReadOnlyProperty { _, _ -> entry }
+}
+
+fun <T : CodecConfig<T>> EntryAddable.register(
+ fieldName: String? = null,
+ configInstance: T
+) = PropertyDelegateProvider<EntryAddable, T> { thisRef, property ->
+ thisRef.register(fieldName ?: property.name, configInstance)
+ configInstance
+}
+
+operator fun <T : CodecConfig<T>> T.getValue(thisRef: CodecConfig<*>?, property: KProperty<*>): T {
+ return this
+}
+
+@get:ApiStatus.Experimental
+@set:ApiStatus.Experimental
+var <T> ConfigEntry<T>.value: T
+ get() = this.get()
+ set(value) = this.set(value)
+
+@get:ApiStatus.Experimental
+val <T> ConfigEntry<T>.default: T
+ get() = this.defaultValue()
+
+@get:ApiStatus.Experimental
+val ConfigEntry<*>.fieldName: String
+ get() = this.fieldName()
+
+@ApiStatus.Experimental
+fun <T : Any> OptionRegistrar.register(
+ configEntry: ConfigEntry<T>,
+ block: OptionDsl<T>.() -> Unit
+) = register<T>(configEntry.fieldName) {
+ binding(configEntry.asBinding())
+ block()
+}
diff --git a/src/main/java/dev/isxander/yacl3/config/v3/ReadonlyConfigEntry.java b/src/main/java/dev/isxander/yacl3/config/v3/ReadonlyConfigEntry.java
new file mode 100644
index 0000000..c38853b
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/config/v3/ReadonlyConfigEntry.java
@@ -0,0 +1,26 @@
+package dev.isxander.yacl3.config.v3;
+
+import com.mojang.serialization.DynamicOps;
+import com.mojang.serialization.RecordBuilder;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.function.Consumer;
+import java.util.function.UnaryOperator;
+
+@ApiStatus.Experimental
+public interface ReadonlyConfigEntry<T> {
+ String fieldName();
+
+ T get();
+
+ ReadonlyConfigEntry<T> modifyGet(UnaryOperator<T> modifier);
+ default ReadonlyConfigEntry<T> onGet(Consumer<T> consumer) {
+ return this.modifyGet(v -> {
+ consumer.accept(v);
+ return v;
+ });
+ }
+
+ <R> RecordBuilder<R> encode(DynamicOps<R> ops, RecordBuilder<R> recordBuilder);
+ <R> boolean decode(R encoded, DynamicOps<R> ops);
+}
diff --git a/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
index e6ab3f1..8def3b3 100644
--- a/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
@@ -100,12 +100,12 @@ public abstract class AbstractWidget implements GuiEventListener, Renderable, Na
vertex.addVertex(matrix4f, x1, y2, 0).setColor(startColor);
vertex.addVertex(matrix4f, x2, y2, 0).setColor(endColor);
vertex.addVertex(matrix4f, x2, y1, 0).setColor(endColor);
- *//*? } else { */
+ *//*?} else {*/
vertex.vertex(matrix4f, x1, y1, 0).color(startColor).endVertex();
vertex.vertex(matrix4f, x1, y2, 0).color(startColor).endVertex();
vertex.vertex(matrix4f, x2, y2, 0).color(endColor).endVertex();
vertex.vertex(matrix4f, x2, y1, 0).color(endColor).endVertex();
- /*? } */
+ /*?}*/
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
index be95caa..8412cc8 100644
--- a/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
+++ b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
@@ -25,8 +25,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
public ElementListWidgetExt(Minecraft client, int x, int y, int width, int height, boolean smoothScrolling) {
/*? if >1.20.2 {*/
super(client, width, x, y, height);
- /*? } else {*//*
- super(client, width, height, y, y + height, 22);
+ /*?} else {*/
+ /*super(client, width, height, y, y + height, 22);
this.x0 = x;
this.x1 = x + width;
*//*?}*/
@@ -53,10 +53,10 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
}
@Override
- /*? if >1.20.2 { */
+ /*? if >1.20.2 {*/
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta)
- /*?} else { *//*
- public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta)
+ /*?} else {*/
+ /*public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta)
*//*?}*/
{
if (usingScrollbar) {
@@ -73,10 +73,10 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
graphics.enableScissor(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight());
- /*? if >1.20.2 { */
+ /*? if >1.20.2 {*/
super.renderWidget(graphics, mouseX, mouseY, delta);
- /*?} else { *//*
- super.render(graphics, mouseX, mouseY, delta);
+ /*?} else {*/
+ /*super.render(graphics, mouseX, mouseY, delta);
*//*?}*/
graphics.disableScissor();
@@ -84,7 +84,7 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
returnSmoothAmount = false;
}
- /*? if >1.20.1 { */
+ /*? if >1.20.1 {*/
@Override
/*?}*/
protected boolean isValidMouseClick(int button) {
@@ -197,8 +197,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
@Override
/*? if >1.20.4 {*/
protected void renderListItems(GuiGraphics graphics, int mouseX, int mouseY, float delta)
- /*? } else {*//*
- protected void renderList(GuiGraphics graphics, int mouseX, int mouseY, float delta)
+ /*?} else {*/
+ /*protected void renderList(GuiGraphics graphics, int mouseX, int mouseY, float delta)
*//*?}*/
{
int left = this.getRowLeft();
@@ -252,8 +252,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
}
}
- /*? if <1.20.3 {*//*
- @Override
+ /*? if <1.20.3 {*/
+ /*@Override
public int getX() {
return x0;
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
index 04f1b67..90bc75f 100644
--- a/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
+++ b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
@@ -123,8 +123,8 @@ public class YACLScreen extends Screen {
currentPopupController = null;
}
- /*? if <=1.20.4 {*//*
- @Override
+ /*? if <=1.20.4 {*/
+ /*@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
renderDirtBackground(graphics);
super.render(graphics, mouseX, mouseY, delta);
diff --git a/src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java b/src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java
index 33028d7..7c1e7e1 100644
--- a/src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java
+++ b/src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java
@@ -13,11 +13,10 @@ public class YACLTooltip extends Tooltip {
this.widget = widget;
}
- /*? if >1.20.4 {*/ // stonecutter cannot handle AND expressions
- /*? } elif >1.20.1 {*//*
- @Override
+ //? if >1.20.1 && <=1.20.4 {
+ /*@Override
protected ClientTooltipPositioner createTooltipPositioner(boolean bl, boolean bl2, ScreenRectangle screenRectangle) {
return new YACLTooltipPositioner(widget);
}
- *//*?}*/
+ *///?}
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
index 8830f20..efa1aec 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
@@ -16,8 +16,8 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
/*? if >1.20.1 {*/
private static final ResourceLocation COLOR_PICKER_LOCATION = YACLPlatform.rl("controller/colorpicker");
private static final ResourceLocation TRANSPARENT_TEXTURE_LOCATION = YACLPlatform.rl("controller/transparent");
- /*? } else {*//*
- // nineslice and repeating only work on a 256x atlas
+ /*?} else {*/
+ /*// nineslice and repeating only work on a 256x atlas
private static final ResourceLocation COLOR_PICKER_ATLAS = YACLPlatform.rl("textures/gui/colorpicker-atlas.png");
*//*?}*/
@@ -88,10 +88,10 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
graphics.pose().translate(0, 0, 10); // render over text
//Background
- /*? if >1.20.3 { */
+ /*? if >1.20.3 {*/
graphics.blitSprite(COLOR_PICKER_LOCATION, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10);
- /*? } else {*//*
- graphics.blitNineSliced(COLOR_PICKER_ATLAS, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10, 3, 236, 34, 0, 0);
+ /*?} else {*/
+ /*graphics.blitNineSliced(COLOR_PICKER_ATLAS, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10, 3, 236, 34, 0, 0);
*//*?}*/
//Main color preview
@@ -99,10 +99,10 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
graphics.fill(previewColorDim.x() - outline, previewColorDim.y() - outline, previewColorDim.xLimit() + outline, previewColorDim.yLimit() + outline, Color.black.getRGB());
//transparent texture - must be rendered BEFORE the main color preview
if(controller.allowAlpha()) {
- /*? if >1.20.3 { */
+ /*? if >1.20.3 {*/
graphics.blitSprite(TRANSPARENT_TEXTURE_LOCATION, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height());
- /*? } else {*//*
- graphics.blitRepeating(COLOR_PICKER_ATLAS, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height(), 236, 0, 8, 8);
+ /*?} else {*/
+ /*graphics.blitRepeating(COLOR_PICKER_ATLAS, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height(), 236, 0, 8, 8);
*//*?}*/
}
//main color preview
@@ -134,10 +134,10 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
//outline
graphics.fill(alphaGradientDim.x() - outline, alphaGradientDim.y() - outline, alphaGradientDim.xLimit() + outline, alphaGradientDim.yLimit() + outline, Color.black.getRGB());
//Transparent texture
- /*? if >1.20.3 { */
+ /*? if >1.20.3 {*/
graphics.blitSprite(TRANSPARENT_TEXTURE_LOCATION, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight);
- /*? } else {*//*
- graphics.blitRepeating(COLOR_PICKER_ATLAS, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight, 236, 0, 8, 8);
+ /*?} else {*/
+ /*graphics.blitRepeating(COLOR_PICKER_ATLAS, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight, 236, 0, 8, 8);
*//*?}*/
//Pending color to transparent
fillSidewaysGradient(graphics, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.xLimit(), alphaGradientDim.yLimit(), getRgbWithoutAlpha(), 0x00000000);
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
index 6252291..f799059 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
@@ -63,8 +63,8 @@ public class DropdownWidget<T> extends ControllerPopupWidget<AbstractDropdownCon
graphics.blit(
/*? if >1.20.4 {*/
Screen.MENU_BACKGROUND,
- /*?} else {*//*
- Screen.BACKGROUND_LOCATION,
+ /*?} else {*/
+ /*Screen.BACKGROUND_LOCATION,
*//*?}*/
dropdownDim.x(), dropdownDim.y(), 0,
0.0f, 0.0f,
diff --git a/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java b/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
index b8293fb..aadc249 100644
--- a/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
+++ b/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
@@ -13,15 +13,15 @@ public class YACLRenderHelper {
YACLPlatform.mcRl("widget/button_highlighted"), // !disabled & focused
YACLPlatform.mcRl("widget/slider_highlighted") // disabled & focused
);
- /*?} else {*//*
- private static final ResourceLocation SLIDER_LOCATION = new ResourceLocation("textures/gui/slider.png");
+ /*?} else {*/
+ /*private static final ResourceLocation SLIDER_LOCATION = new ResourceLocation("textures/gui/slider.png");
*//*?}*/
public static void renderButtonTexture(GuiGraphics graphics, int x, int y, int width, int height, boolean enabled, boolean focused) {
/*? if >1.20.1 {*/
graphics.blitSprite(SPRITES.get(enabled, focused), x, y, width, height);
- /*?} else {*//*
- int textureV;
+ /*?} else {*/
+ /*int textureV;
if (enabled) {
textureV = focused ? 60 : 40;
} else {
@@ -36,8 +36,8 @@ public class YACLRenderHelper {
public static ResourceLocation getSpriteLocation(String path) {
/*? if >1.20.3 {*/
return YACLPlatform.rl(path);
- /*? } else {*//*
- return YACLPlatform.rl("textures/gui/sprites/" + path + ".png");
+ /*?} else {*/
+ /*return YACLPlatform.rl("textures/gui/sprites/" + path + ".png");
*//*?}*/
}
}
diff --git a/src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java b/src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java
index e0fe06c..3ad331f 100644
--- a/src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java
+++ b/src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java
@@ -13,8 +13,8 @@ public interface TabNavigationBarAccessor {
/*? if >1.20.4 {*/
@Accessor("layout")
net.minecraft.client.gui.layouts.LinearLayout yacl$getLayout();
- /*? } else {*//*
- @Accessor("layout")
+ /*?} else {*/
+ /*@Accessor("layout")
net.minecraft.client.gui.layouts.GridLayout yacl$getLayout();
*//*?}*/
diff --git a/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java b/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java
index e330324..514c964 100644
--- a/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java
+++ b/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java
@@ -1,6 +1,6 @@
package dev.isxander.yacl3.platform;
-/*?if fabric {*/
+/*? if fabric {*/
import net.fabricmc.loader.api.FabricLoader;
/*?} elif neoforge {*//*
import net.neoforged.fml.loading.FMLEnvironment;
@@ -39,7 +39,7 @@ public final class YACLPlatform {
}
public static Env getEnvironment() {
- /*?if fabric {*/
+ /*? if fabric {*/
return switch (FabricLoader.getInstance().getEnvironmentType()) {
case CLIENT -> Env.CLIENT;
case SERVER -> Env.SERVER;
@@ -53,7 +53,7 @@ public final class YACLPlatform {
}
public static Path getConfigDir() {
- /*?if fabric {*/
+ /*? if fabric {*/
return FabricLoader.getInstance().getConfigDir();
/*?} elif forge-like {*//*
return FMLPaths.CONFIGDIR.get();
@@ -61,7 +61,7 @@ public final class YACLPlatform {
}
public static boolean isDevelopmentEnv() {
- /*?if fabric {*/
+ /*? if fabric {*/
return FabricLoader.getInstance().isDevelopmentEnvironment();
/*?} elif forge-like {*//*
return !FMLEnvironment.production;
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)
- }
- }
- }
-}
diff --git a/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java b/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java
new file mode 100644
index 0000000..08137e6
--- /dev/null
+++ b/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java
@@ -0,0 +1,53 @@
+package dev.isxander.yacl3.test;
+
+import com.google.gson.JsonElement;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.JsonOps;
+import dev.isxander.yacl3.config.v3.ConfigEntry;
+import dev.isxander.yacl3.config.v3.JsonFileCodecConfig;
+import dev.isxander.yacl3.platform.YACLPlatform;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.ComponentSerialization;
+import net.minecraft.resources.ResourceLocation;
+
+public class CodecConfig extends JsonFileCodecConfig {
+ public static final CodecConfig INSTANCE = new CodecConfig();
+
+ public final ConfigEntry<Integer> myInt =
+ register("my_int", 0, Codec.INT);
+
+ public final ConfigEntry<String> myString =
+ register("my_string", "default", Codec.STRING);
+
+ public final ConfigEntry<ResourceLocation> myIdentifier =
+ register("my_identifier", YACLPlatform.rl("test"), ResourceLocation.CODEC);
+
+ public final ConfigEntry<Component> myText =
+ register("my_text", Component.literal("Hello"), ComponentSerialization.CODEC);
+
+ public final ConfigEntry<InnerCodecConfig> myInnerConfig =
+ register("my_inner_config", InnerCodecConfig.INSTANCE, InnerCodecConfig.INSTANCE);
+
+ public static class InnerCodecConfig extends dev.isxander.yacl3.config.v3.CodecConfig<InnerCodecConfig> {
+ public static final InnerCodecConfig INSTANCE = new InnerCodecConfig();
+ }
+
+ public CodecConfig() {
+ super(YACLPlatform.getConfigDir().resolve("codec_config.json"));
+ }
+
+ void test() {
+ loadFromFile(); // load like this
+ saveToFile(); // save like this
+
+ this.myInt.get();
+ this.myInt.set(5);
+ this.myInt.defaultValue();
+
+ // or if you just extend Config instead of JsonFileConfig:
+ JsonElement element = null;
+ this.decode(element, JsonOps.INSTANCE); // load
+ DataResult<JsonElement> encoded = this.encodeStart(JsonOps.INSTANCE); // save
+ }
+}
diff --git a/src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java b/src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java
index 2c4875f..dca6070 100644
--- a/src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java
+++ b/src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java
@@ -1,4 +1,4 @@
-/*? if neoforge { *//*
+/*? if neoforge {*//*
package dev.isxander.yacl3.test;
import net.neoforged.fml.common.Mod;
diff --git a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
index 07e0098..a515fe0 100644
--- a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
+++ b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
@@ -53,7 +53,7 @@ public class GuiTest {
.option(ButtonOption.createBuilder()
.name(Component.literal("Kotlin DSL Test"))
.action((screen, opt) -> {
- Minecraft.getInstance().setScreen(DslTestKt.kotlinDslGui(screen));
+ Minecraft.getInstance().setScreen(CodecConfigKt.INSTANCE.generateConfigScreen(screen));
})
.build())
.group(OptionGroup.createBuilder()
diff --git a/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
index a3ed7cc..9335fb4 100644
--- a/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
+++ b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
@@ -1,131 +1,146 @@
package dev.isxander.yacl3.test
+import com.mojang.serialization.Codec
import dev.isxander.yacl3.api.OptionFlag
-import dev.isxander.yacl3.api.controller.BooleanControllerBuilder
-import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder
+import dev.isxander.yacl3.config.v3.JsonFileCodecConfig
+import dev.isxander.yacl3.config.v3.register
import dev.isxander.yacl3.dsl.*
import dev.isxander.yacl3.platform.YACLPlatform
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.ComponentSerialization
import net.minecraft.resources.ResourceLocation
-object Foo {
- var bar = true
- var baz = 0
-}
+object CodecConfigKt : JsonFileCodecConfig(YACLPlatform.getConfigDir().resolve("codec_config_kt.json")) {
+ val myInt by register<Int>(0, Codec.INT)
-fun kotlinDslGui(parent: Screen?) = YetAnotherConfigLib("namespace") {
- // default title with translation key:
- // `yacl3.config.namespace.title`
- /* NO CODE REQUIRED */
+ val myString by register<String>("default", Codec.STRING)
- // or set the title
- title(Component.literal("A cool title"))
+ val myBoolean by register<Boolean>(false, Codec.BOOL)
+ val myIdentifier by register<ResourceLocation>(YACLPlatform.rl("test"), ResourceLocation.CODEC)
- // usual save function
- save {
- // run your save function!
- }
+ val myText by register<Component>(Component.literal("Hello, World!"), ComponentSerialization.CODEC)
- // get access to an option from the very root of the dsl!
- categories["testCategory"]["testGroup"].getOption("testOption").onReady {
- // do something with it
+ init {
+ if (!loadFromFile()) {
+ saveToFile()
+ }
}
- val testCategory by categories.registering {
- // default name with translation key:
- // `yacl3.config.namespace.testCategory.testGroup.name`
+ fun generateConfigScreen(lastScreen: Screen?) = YetAnotherConfigLib("namespace") {
+ // default title with translation key:
+ // `yacl3.config.namespace.title`
/* NO CODE REQUIRED */
- // or set the name
- name { Component.literal("A cool category") }
+ // or set the title
+ title(Component.literal("A cool title"))
- // custom tooltip
- tooltipBuilder {
- // add a line like this
- +Component.translatable("somecustomkey")
- // or like this
- text(Component.translatable("somecustomkey"))
+ // usual save function
+ save {
+ // run your save function!
+ saveToFile()
+ }
- // or like this
- text { Component.translatable("somecustomkey") }
+ // get access to an option from the very root of the dsl!
+ categories["testCategory"]["testGroup"].futureRef<String>("myIntOption").onReady {
+ // do something with it
}
- // you can declare things with strings
- group("testGroup") {
+ 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 group") }
+ name { Component.literal("A cool category") }
+
+ // custom tooltip
+ tooltip {
+ // add a line like this
+ +Component.translatable("somecustomkey")
+ // or like this
+ text(Component.translatable("somecustomkey"))
- // custom description builder:
- descriptionBuilder {
- // blah blah blah
+ // or like this
+ text { Component.translatable("somecustomkey") }
}
- // default description with translation key:
- // `yacl3.config.namespace.testCategory.testGroup.description.1-5`
- // not compatible with custom description builder
- useDefaultDescription(lines = 5)
+ // creates a label with the id `testLabel`
+ val testLabel by rootOptions.registeringLabel
- // you can define opts/groups/categories using this delegate syntax
- val testOption by options.registering { // type is automatically inferred from binding
+ // you can declare things with strings
+ groups.register("testGroup") {
// default name with translation key:
- // `yacl3.config.namespace.testCategory.testGroup.testOption.name`
+ // `yacl3.config.namespace.testCategory.testGroup.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)
+ // or set the name
+ name { Component.literal("A cool group") }
+
- text { Component.translatable("somecustomkey") }
- webpImage(YACLPlatform.rl("namespace", "image.png"))
+ // custom description builder:
+ descriptionBuilder {
+ // default description with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.description.1-5`
+ // not compatible with custom description builder
+ addDefaultText(lines = 5)
}
- // KProperties are cool!
- binding(Foo::bar, Foo.bar)
+ // you can define opts/groups/categories using this delegate syntax
+ val myIntOption by options.registering<Int> { // type is automatically inferred from binding
+ // default name with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.testOption.name`
+ /* NO CODE REQUIRED */
- // 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) }
- }
+ // custom description builder:
+ descriptionBuilderDyn { value -> // changes the desc based on the current value
+ // description with translation key:
+ // `yacl3.config.namespace.testCategory.testGroup.testOption.description.1-5`
+ addDefaultText(lines = 5)
- // or even get an access to them before creation
- options.get<Int>("otherTestOption").onReady {
- // do something with it
- }
+ text { Component.translatable("somecustomkey") }
+ webpImage(YACLPlatform.rl("namespace", "image.png"))
+ }
- // you can set available with a block
- available { true }
+ // Codecs!
+ binding = myInt.asBinding()
- // regular controller stuff
- // this will be DSLed at some point
- controller(BooleanControllerBuilder::create) {
- // blah blah blah
- }
+ // you can access other options like this!
+ // `options` field is from the enclosing group dsl
+ listener { opt, newVal ->
+ options.futureRef<String>("myString").onReady {
- // flags as usual
- flag(OptionFlag.ASSET_RELOAD)
- }
+ }
+ }
+
+ // or use a delegated property to create a reference to an option
+ val myStringOption by options.ref<String>() // nullable
+
+ // you can set available with a property
+ available = true
+ // ...or a block
+ available { true }
+
+ // cool custom controller functions
+ controller = slider(range = 5..10, step = 1)
- val otherTestOption by options.registering { // type is automatically inferred from binding
- controller(IntegerSliderControllerBuilder::create) {
- range(0, 100)
- step(5)
+ // flags as usual
+ flag(OptionFlag.ASSET_RELOAD)
}
- binding(Foo::baz, Foo.baz)
+ // codec config api automatically sets binding and name
+ val myStringOption = options.register(myString) {
+ controller = stringField()
+ }
- // blah blah blah other stuff
+ val myBooleanOption = options.register(myBoolean) {
+ // custom formatters for these cool controller functions
+ controller = textSwitch { bool -> Component.literal(bool.toString()) }
+ }
}
}
- }
-}.generateScreen(parent)
+ }.generateScreen(lastScreen)
+}