aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/dev/isxander/yacl3/config
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 /src/main/java/dev/isxander/yacl3/config
parent65b4f7ba8374bbaebc6a431f8347ffc3e8afdced (diff)
downloadYetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.tar.gz
YetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.tar.bz2
YetAnotherConfigLib-305718e163f91802a4bc1c1ed6540febb2ce204e.zip
codec config and rewritten kotlin dsl
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/config')
-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
12 files changed, 486 insertions, 15 deletions
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);
+}