diff options
36 files changed, 1342 insertions, 601 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index 13dcede..5d5cb9a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,13 +2,13 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` - kotlin("jvm") version "1.9.23" + kotlin("jvm") - id("dev.architectury.loom") version "1.6.+" + id("dev.architectury.loom") - id("me.modmuss50.mod-publish-plugin") version "0.5.+" + id("me.modmuss50.mod-publish-plugin") `maven-publish` - id("org.ajoberstar.grgit") version "5.0.+" + id("org.ajoberstar.grgit") } val loader = loom.platform.get().name.lowercase() @@ -20,7 +20,7 @@ val isForgeLike = isNeoforge || isForge val mcVersion = findProperty("mcVersion").toString() group = "dev.isxander" -val versionWithoutMC = "3.4.4" +val versionWithoutMC = "3.5.0" version = "$versionWithoutMC+${stonecutter.current.project}" val snapshotVer = "${grgit.branch.current().name.replace('/', '.')}-SNAPSHOT" @@ -35,19 +35,6 @@ base { archivesName.set(property("modName").toString()) } -stonecutter.expression { - when (it) { - "controlify" -> isPropDefined("deps.controlify") - "mod-menu" -> isPropDefined("deps.modMenu") - "fabric" -> isFabric - "neoforge" -> isNeoforge - "forge" -> isForge - "!forge" -> !isForge - "forge-like" -> isForgeLike - else -> null - } -} - val testmod by sourceSets.creating { compileClasspath += sourceSets.main.get().compileClasspath runtimeClasspath += sourceSets.main.get().runtimeClasspath diff --git a/settings.gradle.kts b/settings.gradle.kts index 9027127..c4edb3d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -import dev.kikugie.stonecutter.gradle.StonecutterSettings +import dev.kikugie.stonecutter.StonecutterSettings pluginManagement { repositories { @@ -9,11 +9,12 @@ pluginManagement { maven("https://maven.neoforged.net/releases/") maven("https://maven.minecraftforge.net/") maven("https://maven.kikugie.dev/releases") + maven("https://maven.kikugie.dev/snapshots") } } plugins { - id("dev.kikugie.stonecutter") version "0.3.5" + id("dev.kikugie.stonecutter") version "0.4-beta.3" } extensions.configure<StonecutterSettings> { @@ -22,7 +23,7 @@ extensions.configure<StonecutterSettings> { shared { fun mc(mcVersion: String, name: String = mcVersion, loaders: Iterable<String>) { for (loader in loaders) { - versions("$name-$loader") + vers("$name-$loader", mcVersion) } } 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) +} diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index d2562de..8aa3e0c 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -1,8 +1,28 @@ plugins { id("dev.kikugie.stonecutter") + id("dev.architectury.loom") version "1.6.+" apply false + + kotlin("jvm") version "1.9.23" apply false + + id("me.modmuss50.mod-publish-plugin") version "0.5.+" apply false + id("org.ajoberstar.grgit") version "5.0.+" apply false } stonecutter active "1.20.6-fabric" /* [SC] DO NOT EDIT */ +stonecutter.configureEach { + val platform = project.property("loom.platform") + + fun String.propDefined() = project.findProperty(this)?.toString()?.isNotBlank() ?: false + consts(listOf( + "fabric" to (platform == "fabric"), + "forge" to (platform == "forge"), + "neoforge" to (platform == "neoforge"), + "forge-like" to (platform == "forge" || platform == "neoforge"), + "controlify" to "deps.controlify".propDefined(), + "mod-menu" to "deps.modMenu".propDefined(), + )) +} + stonecutter registerChiseled tasks.register("chiseledBuild", stonecutter.chiseled) { group = "mod" ofTask("build") |