From 305718e163f91802a4bc1c1ed6540febb2ce204e Mon Sep 17 00:00:00 2001 From: isXander Date: Tue, 11 Jun 2024 23:13:49 +0100 Subject: codec config and rewritten kotlin dsl --- .../isxander/yacl3/config/GsonConfigInstance.java | 20 ++--- .../v2/impl/serializer/GsonConfigSerializer.java | 10 +-- .../yacl3/config/v3/AbstractConfigEntry.java | 48 ++++++++++++ .../config/v3/AbstractReadonlyConfigEntry.java | 37 +++++++++ .../yacl3/config/v3/ChildConfigEntryImpl.java | 49 ++++++++++++ .../dev/isxander/yacl3/config/v3/CodecConfig.java | 77 ++++++++++++++++++ .../yacl3/config/v3/CodecConfigEntryImpl.java | 42 ++++++++++ .../dev/isxander/yacl3/config/v3/ConfigEntry.java | 38 +++++++++ .../dev/isxander/yacl3/config/v3/EntryAddable.java | 11 +++ .../yacl3/config/v3/JsonFileCodecConfig.java | 90 ++++++++++++++++++++++ .../dev/isxander/yacl3/config/v3/KotlinExts.kt | 53 +++++++++++++ .../yacl3/config/v3/ReadonlyConfigEntry.java | 26 +++++++ 12 files changed, 486 insertions(+), 15 deletions(-) create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/AbstractConfigEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/AbstractReadonlyConfigEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/ChildConfigEntryImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/CodecConfig.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/CodecConfigEntryImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/ConfigEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/EntryAddable.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/JsonFileCodecConfig.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/KotlinExts.kt create mode 100644 src/main/java/dev/isxander/yacl3/config/v3/ReadonlyConfigEntry.java (limited to 'src/main/java/dev/isxander/yacl3/config') 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 extends ConfigInstance { 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 extends ConfigInstance { private UnaryOperator 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 extends ConfigSerializer { private UnaryOperator 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 extends AbstractReadonlyConfigEntry implements ConfigEntry { + private T value; + private final T defaultValue; + + private Function 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 modifyGet(UnaryOperator modifier) { + super.modifyGet(modifier); + return this; + } + + @Override + public ConfigEntry modifySet(UnaryOperator 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 implements ReadonlyConfigEntry { + private final String fieldName; + + private Function 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 modifyGet(UnaryOperator 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> extends AbstractReadonlyConfigEntry { + private final T config; + private final MapCodec 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 RecordBuilder encode(DynamicOps ops, RecordBuilder recordBuilder) { + return mapCodec.encode(config, ops, recordBuilder); + } + + @Override + public boolean decode(R encoded, DynamicOps ops) { + DataResult result = mapCodec.decoder().parse(ops, encoded); + + //? if >1.20.4 { + Optional> error = result.error(); + //?} else { + /*Optional> 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> implements EntryAddable, Codec { + private final List> entries = new ArrayList<>(); + + public CodecConfig() { + // cast here to throw immediately on construction + var ignored = (S) this; + } + + @Override + public ConfigEntry register(String fieldName, T defaultValue, Codec codec) { + ConfigEntry entry = new CodecConfigEntryImpl<>(fieldName, defaultValue, codec); + entries.add(entry); + return entry; + } + + @Override + public > ReadonlyConfigEntry register(String fieldName, T configInstance) { + ReadonlyConfigEntry entry = new ChildConfigEntryImpl<>(fieldName, configInstance); + entries.add(entry); + return entry; + } + + protected void onFinishedDecode(boolean successful) { + } + + @Override + public DataResult encode(S input, DynamicOps 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 DataResult> decode(DynamicOps ops, R input) { + this.decode(input, ops); + return DataResult.success(Pair.of((S) this, input)); + } + + public final DataResult encode(DynamicOps ops, R prefix) { + RecordBuilder builder = ops.mapBuilder(); + for (ReadonlyConfigEntry entry : entries) { + builder = entry.encode(ops, builder); + } + return builder.build(prefix); + } + + public final DataResult encodeStart(DynamicOps ops) { + return this.encode(ops, ops.empty()); + } + + /** + * @return true if decoding of all entries was successful + */ + public final boolean decode(R encoded, DynamicOps 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 extends AbstractConfigEntry { + private final MapCodec mapCodec; + + public CodecConfigEntryImpl(String fieldName, T defaultValue, Codec codec) { + super(fieldName, defaultValue); + this.mapCodec = codec.fieldOf(this.fieldName()); + } + + @Override + public RecordBuilder encode(DynamicOps ops, RecordBuilder recordBuilder) { + return mapCodec.encode(get(), ops, recordBuilder); + } + + @Override + public boolean decode(R encoded, DynamicOps ops) { + DataResult result = mapCodec.decoder().parse(ops, encoded); + + //? if >1.20.4 { + Optional> error = result.error(); + //?} else { + /*Optional> 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 extends ReadonlyConfigEntry { + + void set(T value); + + T defaultValue(); + + @Override + ConfigEntry modifyGet(UnaryOperator modifier); + + @Override + default ConfigEntry onGet(Consumer consumer) { + return this.modifyGet(v -> { + consumer.accept(v); + return v; + }); + } + + ConfigEntry modifySet(UnaryOperator modifier); + default ConfigEntry onSet(Consumer consumer) { + return this.modifySet(v -> { + consumer.accept(v); + return v; + }); + } + + default Binding 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 { + ConfigEntry register(String fieldName, T defaultValue, Codec codec); + + > ReadonlyConfigEntry 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 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 EntryAddable.register( + default: T, + codec: Codec +) = PropertyDelegateProvider>> { thisRef, property -> + val entry = thisRef.register(property.name, default, codec) + ReadOnlyProperty { _, _ -> entry } +} + +fun > EntryAddable.register( + fieldName: String? = null, + configInstance: T +) = PropertyDelegateProvider { thisRef, property -> + thisRef.register(fieldName ?: property.name, configInstance) + configInstance +} + +operator fun > T.getValue(thisRef: CodecConfig<*>?, property: KProperty<*>): T { + return this +} + +@get:ApiStatus.Experimental +@set:ApiStatus.Experimental +var ConfigEntry.value: T + get() = this.get() + set(value) = this.set(value) + +@get:ApiStatus.Experimental +val ConfigEntry.default: T + get() = this.defaultValue() + +@get:ApiStatus.Experimental +val ConfigEntry<*>.fieldName: String + get() = this.fieldName() + +@ApiStatus.Experimental +fun OptionRegistrar.register( + configEntry: ConfigEntry, + block: OptionDsl.() -> Unit +) = register(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 { + String fieldName(); + + T get(); + + ReadonlyConfigEntry modifyGet(UnaryOperator modifier); + default ReadonlyConfigEntry onGet(Consumer consumer) { + return this.modifyGet(v -> { + consumer.accept(v); + return v; + }); + } + + RecordBuilder encode(DynamicOps ops, RecordBuilder recordBuilder); + boolean decode(R encoded, DynamicOps ops); +} -- cgit