aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xander@isxander.dev>2024-06-11 23:13:49 +0100
committerisXander <xander@isxander.dev>2024-06-11 23:13:57 +0100
commit305718e163f91802a4bc1c1ed6540febb2ce204e (patch)
treed72fe8b95dab1ef89f67b13a19f8c06fdb582c28
parent65b4f7ba8374bbaebc6a431f8347ffc3e8afdced (diff)
downloadYetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.tar.gz
YetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.tar.bz2
YetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.zip
codec config and rewritten kotlin dsl
-rw-r--r--build.gradle.kts23
-rw-r--r--settings.gradle.kts7
-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
-rw-r--r--stonecutter.gradle.kts20
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/isx