aboutsummaryrefslogtreecommitdiff
path: root/common/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'common/src/main/java')
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java6
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java34
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java13
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java14
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java68
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java9
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java101
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java74
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java36
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java152
-rw-r--r--common/src/main/java/dev/isxander/yacl3/platform/Env.java10
-rw-r--r--common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java22
15 files changed, 592 insertions, 0 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java b/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java
index 15ce5bc..5213e90 100644
--- a/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java
+++ b/common/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java
@@ -2,6 +2,7 @@ package dev.isxander.yacl3.api;
import com.google.common.collect.ImmutableList;
import dev.isxander.yacl3.config.ConfigInstance;
+import dev.isxander.yacl3.config.v2.api.ConfigClassHandler;
import dev.isxander.yacl3.gui.YACLScreen;
import dev.isxander.yacl3.impl.YetAnotherConfigLibImpl;
import net.minecraft.client.gui.screens.Screen;
@@ -51,10 +52,15 @@ public interface YetAnotherConfigLib {
return new YetAnotherConfigLibImpl.BuilderImpl();
}
+ static <T> YetAnotherConfigLib create(ConfigClassHandler<T> configHandler, ConfigBackedBuilder<T> builder) {
+ return builder.build(configHandler.defaults(), configHandler.instance(), createBuilder().save(configHandler.serializer()::serialize)).build();
+ }
+
/**
* Creates an instance using a {@link ConfigInstance} which autofills the save() builder method.
* This also takes an easy functional interface that provides defaults and config to help build YACL bindings.
*/
+ @Deprecated
static <T> YetAnotherConfigLib create(ConfigInstance<T> configInstance, ConfigBackedBuilder<T> builder) {
return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build();
}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java
new file mode 100644
index 0000000..22e471f
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java
@@ -0,0 +1,34 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import dev.isxander.yacl3.api.YetAnotherConfigLib;
+import dev.isxander.yacl3.config.v2.impl.ConfigClassHandlerImpl;
+
+import java.util.function.Function;
+
+public interface ConfigClassHandler<T> {
+ T instance();
+
+ T defaults();
+
+ Class<T> configClass();
+
+ ConfigField<?>[] fields();
+
+ YetAnotherConfigLib generateGui();
+
+ boolean supportsAutoGen();
+
+ ConfigSerializer<T> serializer();
+
+ static <T> Builder<T> createBuilder(Class<T> configClass) {
+ return new ConfigClassHandlerImpl.BuilderImpl<>(configClass);
+ }
+
+ interface Builder<T> {
+ Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory);
+
+ Builder<T> autoGen(boolean autoGen);
+
+ ConfigClassHandler<T> build();
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java
new file mode 100644
index 0000000..8b95c3f
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigEntry.java
@@ -0,0 +1,18 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import dev.isxander.yacl3.config.v2.impl.DefaultOptionFactory;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface ConfigEntry {
+ Class<? extends OptionFactory<?>> factory() default DefaultOptionFactory.class;
+
+ String serialName() default "";
+
+ String comment() default "";
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java
new file mode 100644
index 0000000..26a309f
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java
@@ -0,0 +1,17 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public interface ConfigField<T> {
+ String serialName();
+
+ Optional<String> comment();
+
+ FieldAccess<T> access();
+
+ @Nullable OptionFactory<T> factory();
+
+ boolean supportsFactory();
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java
new file mode 100644
index 0000000..999221d
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java
@@ -0,0 +1,13 @@
+package dev.isxander.yacl3.config.v2.api;
+
+public abstract class ConfigSerializer<T> {
+ protected final ConfigClassHandler<T> config;
+
+ public ConfigSerializer(ConfigClassHandler<T> config) {
+ this.config = config;
+ }
+
+ public abstract void serialize();
+
+ public abstract void deserialize();
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java
new file mode 100644
index 0000000..aed9801
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java
@@ -0,0 +1,14 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import java.lang.reflect.Type;
+
+public interface FieldAccess<T> {
+ T get();
+
+ void set(T value);
+
+ String name();
+
+ Type type();
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java
new file mode 100644
index 0000000..8d8d6c7
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/GsonConfigSerializerBuilder.java
@@ -0,0 +1,68 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import dev.isxander.yacl3.config.ConfigEntry;
+import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.Style;
+
+import java.awt.*;
+import java.nio.file.Path;
+import java.util.function.UnaryOperator;
+
+public interface GsonConfigSerializerBuilder<T> {
+ static <T> GsonConfigSerializerBuilder<T> create(ConfigClassHandler<T> config) {
+ return new GsonConfigSerializer.Builder<>(config);
+ }
+
+ /**
+ * Sets the file path to save and load the config from.
+ */
+ GsonConfigSerializerBuilder<T> setPath(Path path);
+
+ /**
+ * Sets the GSON instance to use. Overrides all YACL defaults such as:
+ * <ul>
+ * <li>lower_camel_case field naming policy</li>
+ * <li>null serialization</li>
+ * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
+ * </ul>
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry}
+ * but these can be added to with setExclusionStrategies.
+ *
+ * @param gsonBuilder gson builder to use
+ */
+ GsonConfigSerializerBuilder<T> overrideGsonBuilder(GsonBuilder gsonBuilder);
+
+ /**
+ * Sets the GSON instance to use. Overrides all YACL defaults such as:
+ * <ul>
+ * <li>lower_camel_case field naming policy</li>
+ * <li>null serialization</li>
+ * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
+ * </ul>
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry}
+ * but these can be added to with setExclusionStrategies.
+ *
+ * @param gson gson instance to be converted to a builder
+ */
+ GsonConfigSerializerBuilder<T> overrideGsonBuilder(Gson gson);
+
+ /**
+ * Appends extra configuration to a GSON builder.
+ * This is the intended way to add functionality to the GSON instance.
+ * <p>
+ * By default, YACL sets the GSON with the following options:
+ * <ul>
+ * <li>lower_camel_case field naming policy</li>
+ * <li>null serialization</li>
+ * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
+ * </ul>
+ *
+ * @param gsonBuilder the function to apply to the builder
+ */
+ GsonConfigSerializerBuilder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder);
+
+ ConfigSerializer<T> build();
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java
new file mode 100644
index 0000000..aabfcf0
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/OptionFactory.java
@@ -0,0 +1,9 @@
+package dev.isxander.yacl3.config.v2.api;
+
+import dev.isxander.yacl3.api.Option;
+
+public interface OptionFactory<T> {
+ Option<T> create(ConfigField<T> field);
+
+ Class<T> type();
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java
new file mode 100644
index 0000000..62aa9b6
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java
@@ -0,0 +1,101 @@
+package dev.isxander.yacl3.config.v2.impl;
+
+import dev.isxander.yacl3.api.YetAnotherConfigLib;
+import dev.isxander.yacl3.config.v2.api.*;
+import dev.isxander.yacl3.platform.YACLPlatform;
+import org.apache.commons.lang3.Validate;
+
+import java.lang.reflect.Constructor;
+import java.util.Arrays;
+import java.util.function.Function;
+
+public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
+ private final Class<T> configClass;
+ private final boolean supportsAutoGen;
+ private final ConfigSerializer<T> serializer;
+ private final ConfigField<?>[] fields;
+
+ private final T instance, defaults;
+
+ public ConfigClassHandlerImpl(Class<T> configClass, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory, boolean autoGen) {
+ this.configClass = configClass;
+ this.supportsAutoGen = YACLPlatform.getEnvironment().isClient() && autoGen;
+
+ try {
+ Constructor<T> constructor = configClass.getDeclaredConstructor();
+ this.instance = constructor.newInstance();
+ this.defaults = constructor.newInstance();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to create instance of config class '%s' with no-args constructor.".formatted(configClass.getName()), e);
+ }
+
+ this.fields = Arrays.stream(configClass.getDeclaredFields())
+ .filter(field -> field.isAnnotationPresent(ConfigEntry.class))
+ .map(field -> new ConfigFieldImpl<>(this.supportsAutoGen(), field.getAnnotation(ConfigEntry.class), new ReflectionFieldAccess<>(field, instance)))
+ .toArray(ConfigField[]::new);
+ this.serializer = serializerFactory.apply(this);
+ }
+
+ @Override
+ public T instance() {
+ return this.instance;
+ }
+
+ @Override
+ public T defaults() {
+ return this.defaults;
+ }
+
+ @Override
+ public Class<T> configClass() {
+ return this.configClass;
+ }
+
+ @Override
+ public ConfigField<?>[] fields() {
+ return this.fields;
+ }
+
+ @Override
+ public boolean supportsAutoGen() {
+ return this.supportsAutoGen;
+ }
+
+ @Override
+ public YetAnotherConfigLib generateGui() {
+ Validate.isTrue(supportsAutoGen(), "Auto GUI generation is not supported for this config class. You either need to enable it in the builder or you are attempting to create a GUI in a dedicated server environment.");
+
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public ConfigSerializer<T> serializer() {
+ return this.serializer;
+ }
+
+ public static class BuilderImpl<T> implements Builder<T> {
+ private final Class<T> configClass;
+ private Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory;
+ private boolean autoGen;
+
+ public BuilderImpl(Class<T> configClass) {
+ this.configClass = configClass;
+ }
+
+ @Override
+ public Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) {
+ this.serializerFactory = serializerFactory;
+ return this;
+ }
+
+ @Override
+ public Builder<T> autoGen(boolean autoGen) {
+ throw new IllegalArgumentException();
+ }
+
+ @Override
+ public ConfigClassHandler<T> build() {
+ return new ConfigClassHandlerImpl<>(configClass, serializerFactory, autoGen);
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java
new file mode 100644
index 0000000..68bf4b8
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java
@@ -0,0 +1,74 @@
+package dev.isxander.yacl3.config.v2.impl;
+
+import dev.isxander.yacl3.config.v2.api.ConfigEntry;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.FieldAccess;
+import dev.isxander.yacl3.config.v2.api.OptionFactory;
+import org.apache.commons.lang3.NotImplementedException;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Constructor;
+import java.util.Optional;
+
+public class ConfigFieldImpl<T> implements ConfigField<T> {
+ private final @Nullable OptionFactory<T> factory;
+ private final String serialName;
+ private final Optional<String> comment;
+ private final FieldAccess<T> field;
+ private final boolean autoGen;
+
+ public ConfigFieldImpl(boolean auto, ConfigEntry entry, FieldAccess<T> field) {
+ this.serialName = "".equals(entry.serialName()) ? field.name() : entry.serialName();
+ this.comment = "".equals(entry.comment()) ? Optional.empty() : Optional.of(entry.comment());
+ this.factory = auto ? makeFactory(entry.factory(), this.serialName) : null;
+ this.autoGen = auto;
+ this.field = field;
+ }
+
+ @Override
+ public String serialName() {
+ return this.serialName;
+ }
+
+ @Override
+ public Optional<String> comment() {
+ return this.comment;
+ }
+
+ @Override
+ public FieldAccess<T> access() {
+ return field;
+ }
+
+ @Override
+ public @Nullable OptionFactory<T> factory() {
+ return factory;
+ }
+
+ @Override
+ public boolean supportsFactory() {
+ return this.autoGen;
+ }
+
+ private OptionFactory<T> makeFactory(Class<? extends OptionFactory<?>> clazz, String name) {
+ if (clazz.equals(DefaultOptionFactory.class)) {
+ throw new NotImplementedException("Field '%s' does not have an option factory, but auto-gen is enabled.".formatted(this.serialName()));
+ }
+
+ Constructor<?> constructor;
+
+ try {
+ constructor = clazz.getConstructor(String.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Failed to find (String) constructor for option factory %s.".formatted(clazz.getName()), e);
+ }
+
+ try {
+ return (OptionFactory<T>) constructor.newInstance(name);
+ } catch (ClassCastException e) {
+ throw new IllegalStateException("Failed to cast option factory %s to OptionFactory<%s>.".formatted(clazz.getName(), field.type().getTypeName()), e);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Failed to create new option factory (class is '%s')".formatted(clazz.getName()), e);
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java
new file mode 100644
index 0000000..e32de00
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/DefaultOptionFactory.java
@@ -0,0 +1,18 @@
+package dev.isxander.yacl3.config.v2.impl;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.OptionFactory;
+import org.apache.commons.lang3.NotImplementedException;
+
+public class DefaultOptionFactory implements OptionFactory<Object> {
+ @Override
+ public Option<Object> create(ConfigField<Object> field) {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public Class<Object> type() {
+ throw new NotImplementedException();
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java
new file mode 100644
index 0000000..114137f
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java
@@ -0,0 +1,36 @@
+package dev.isxander.yacl3.config.v2.impl;
+
+import dev.isxander.yacl3.config.v2.api.FieldAccess;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+
+public record ReflectionFieldAccess<T>(Field field, Object instance) implements FieldAccess<T> {
+ @Override
+ public T get() {
+ try {
+ return (T) field.get(instance);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void set(T value) {
+ try {
+ field.set(instance, value);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String name() {
+ return field.getName();
+ }
+
+ @Override
+ public Type type() {
+ return field.getGenericType();
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
new file mode 100644
index 0000000..8bbc079
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
@@ -0,0 +1,152 @@
+package dev.isxander.yacl3.config.v2.impl.serializer;
+
+import com.google.gson.*;
+import dev.isxander.yacl3.config.v2.api.ConfigClassHandler;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.ConfigSerializer;
+import dev.isxander.yacl3.config.v2.api.GsonConfigSerializerBuilder;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
+import dev.isxander.yacl3.platform.YACLPlatform;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.Style;
+
+import java.awt.*;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+
+public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
+ private final Gson gson;
+ private final Path path;
+
+ private GsonConfigSerializer(ConfigClassHandler<T> config, Path path, Gson gson) {
+ super(config);
+ this.gson = gson;
+ this.path = path;
+ }
+
+ @Override
+ public void serialize() {
+ JsonObject root = new JsonObject();
+
+ for (ConfigField<?> field : config.fields()) {
+ if (YACLPlatform.isDevelopmentEnv() && field.comment().isPresent()) {
+ YACLConstants.LOGGER.error("Config field '{}' has a comment, but comments are not supported by Gson. Please remove the comment or switch to a different serializer. This log will not be shown in production.", field.serialName());
+ }
+
+ try {
+ root.add(field.serialName(), gson.toJsonTree(field.access().get()));
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to serialize config field '{}'.", field.serialName(), e);
+ }
+ }
+
+ YACLConstants.LOGGER.info("Serializing {} to '{}'", config.configClass(), path);
+ try {
+ Files.writeString(path, gson.toJson(root), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to serialize config class '{}'.", config.configClass().getSimpleName(), e);
+ }
+ }
+
+ @Override
+ public void deserialize() {
+ if (!Files.exists(path)) {
+ YACLConstants.LOGGER.info("Config file '{}' does not exist. Creating it with default values.", path);
+ serialize();
+ return;
+ }
+
+ YACLConstants.LOGGER.info("Deserializing {} from '{}'", config.configClass().getSimpleName(), path);
+
+ String json;
+ try {
+ json = Files.readString(path);
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to read '%s' for deserialization.".formatted(path), e);
+ }
+
+ Map<String, JsonElement> root = gson.fromJson(json, JsonObject.class).asMap();
+ List<String> unconsumedKeys = new ArrayList<>(root.keySet());
+
+ for (ConfigField<?> field : config.fields()) {
+ if (root.containsKey(field.serialName())) {
+ try {
+ field.access().set(gson.fromJson(root.get(field.serialName()), field.access().type()));
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to deserialize config field '{}'.", field.serialName(), e);
+ }
+ } else {
+ YACLConstants.LOGGER.warn("Config field '{}' was not found in the config file. Skipping.", field.serialName());
+ }
+
+ unconsumedKeys.remove(field.serialName());
+ }
+
+ if (!unconsumedKeys.isEmpty()) {
+ YACLConstants.LOGGER.warn("The following keys were not consumed by the config class: {}", String.join(", ", unconsumedKeys));
+ }
+ }
+
+ public static class ColorTypeAdapter implements JsonSerializer<Color>, JsonDeserializer<Color> {
+ @Override
+ public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ return new Color(jsonElement.getAsInt(), true);
+ }
+
+ @Override
+ public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(color.getRGB());
+ }
+ }
+
+ public static class Builder<T> implements GsonConfigSerializerBuilder<T> {
+ private final ConfigClassHandler<T> config;
+ private Path path;
+ private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .serializeNulls()
+ .registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
+ .registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
+ .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .setPrettyPrinting();
+
+ public Builder(ConfigClassHandler<T> config) {
+ this.config = config;
+ }
+
+ @Override
+ public Builder<T> setPath(Path path) {
+ this.path = path;
+ return this;
+ }
+
+ @Override
+ public Builder<T> overrideGsonBuilder(GsonBuilder gsonBuilder) {
+ this.gsonBuilder = builder -> gsonBuilder;
+ return this;
+ }
+
+ @Override
+ public Builder<T> overrideGsonBuilder(Gson gson) {
+ return this.overrideGsonBuilder(gson.newBuilder());
+ }
+
+ @Override
+ public Builder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder) {
+ UnaryOperator<GsonBuilder> prev = this.gsonBuilder;
+ this.gsonBuilder = builder -> gsonBuilder.apply(prev.apply(builder));
+ return this;
+ }
+
+ @Override
+ public GsonConfigSerializer<T> build() {
+ return new GsonConfigSerializer<>(config, path, gsonBuilder.apply(new GsonBuilder()).create());
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/platform/Env.java b/common/src/main/java/dev/isxander/yacl3/platform/Env.java
new file mode 100644
index 0000000..276d294
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/platform/Env.java
@@ -0,0 +1,10 @@
+package dev.isxander.yacl3.platform;
+
+public enum Env {
+ CLIENT,
+ SERVER;
+
+ public boolean isClient() {
+ return this == Env.CLIENT;
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java b/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java
new file mode 100644
index 0000000..590723e
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java
@@ -0,0 +1,22 @@
+package dev.isxander.yacl3.platform;
+
+import dev.architectury.injectables.annotations.ExpectPlatform;
+
+import java.nio.file.Path;
+
+public final class YACLPlatform {
+ @ExpectPlatform
+ public static Env getEnvironment() {
+ throw new AssertionError();
+ }
+
+ @ExpectPlatform
+ public static Path getConfigDir() {
+ throw new AssertionError();
+ }
+
+ @ExpectPlatform
+ public static boolean isDevelopmentEnv() {
+ throw new AssertionError();
+ }
+}