aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xandersmith2008@gmail.com>2023-08-12 23:56:06 +0100
committerisXander <xandersmith2008@gmail.com>2023-09-28 16:22:52 +0100
commite34aa9d2a9e38a2773c61158beaed70bbf90a013 (patch)
tree8e44acfe0905aa600360608489cf1b6ba8609890
parent3a7fc265399b4a10a6ae499b0d015736e5464cba (diff)
downloadYetAnotherConfigLib-e34aa9d2a9e38a2773c61158beaed70bbf90a013.tar.gz
YetAnotherConfigLib-e34aa9d2a9e38a2773c61158beaed70bbf90a013.tar.bz2
YetAnotherConfigLib-e34aa9d2a9e38a2773c61158beaed70bbf90a013.zip
Better error handling for config API save/load. More customization on SerialEntry.
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java14
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java42
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java11
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java4
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java13
-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.java20
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java85
-rw-r--r--test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java7
9 files changed, 260 insertions, 37 deletions
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
index 470eba0..d94280f 100644
--- 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
@@ -55,10 +55,24 @@ public interface ConfigClassHandler<T> {
boolean supportsAutoGen();
/**
+ * Safely loads the config class using the provided serializer.
+ * @return if the config was loaded successfully
+ */
+ boolean load();
+
+ /**
+ * Safely saves the config class using the provided serializer.
+ */
+ void save();
+
+ /**
* The serializer for this config class.
* Manages saving and loading of the config with fields
* annotated with {@link SerialEntry}.
+ *
+ * @deprecated use {@link #load()} and {@link #save()} instead.
*/
+ @Deprecated
ConfigSerializer<T> serializer();
/**
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
index 13d6e08..4ac988c 100644
--- 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
@@ -1,9 +1,11 @@
package dev.isxander.yacl3.config.v2.api;
+import java.util.Map;
+
/**
* The base class for config serializers,
* offering a method to save and load.
- * @param <T>
+ * @param <T> the config class to be (de)serialized
*/
public abstract class ConfigSerializer<T> {
protected final ConfigClassHandler<T> config;
@@ -20,7 +22,43 @@ public abstract class ConfigSerializer<T> {
public abstract void save();
/**
+ * Loads all fields into the config class.
+ * @param bufferAccessMap a map of the field accesses. instead of directly setting the field with
+ * {@link ConfigField#access()}, use this parameter. This loads into a temporary object,
+ * and the class handler handles pushing these changes to the instance.
+ * @return the result of the load
+ */
+ public LoadResult loadSafely(Map<ConfigField<?>, FieldAccess<?>> bufferAccessMap) {
+ this.load();
+ return LoadResult.NO_CHANGE;
+ }
+
+ /**
* Loads all fields in the config class.
+ *
+ * @deprecated use {@link #loadSafely(Map)} instead.
*/
- public abstract void load();
+ @Deprecated
+ public void load() {
+ throw new IllegalArgumentException("load() is deprecated, use loadSafely() instead.");
+ }
+
+ public enum LoadResult {
+ /**
+ * Indicates that the config was loaded successfully and the temporary object should be applied.
+ */
+ SUCCESS,
+ /**
+ * Indicates that the config was not loaded successfully and the load should be abandoned.
+ */
+ FAILURE,
+ /**
+ * Indicates that the config has not changed after a load and the temporary object should be ignored.
+ */
+ NO_CHANGE,
+ /**
+ * Indicates the config was loaded successfully, but the config should be re-saved straight away.
+ */
+ DIRTY
+ }
}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java
index d87283a..94bf785 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java
@@ -25,4 +25,15 @@ public @interface SerialEntry {
* If empty, the serializer will not add a comment.
*/
String comment() default "";
+
+ /**
+ * Whether the field is required in the loaded config to be valid.
+ * If it's not, the config will be marked as dirty and re-saved with the default value.
+ */
+ boolean required() default true;
+
+ /**
+ * Whether the field can be null.
+ */
+ boolean nullable() default false;
}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java
index 6337565..cf6abfc 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java
@@ -9,4 +9,8 @@ public interface SerialField {
String serialName();
Optional<String> comment();
+
+ boolean required();
+
+ boolean nullable();
}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java
index 49b3999..33003d7 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java
@@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder;
import dev.isxander.yacl3.config.ConfigEntry;
import dev.isxander.yacl3.config.v2.api.ConfigClassHandler;
import dev.isxander.yacl3.config.v2.api.ConfigSerializer;
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
@@ -55,7 +56,6 @@ public interface GsonConfigSerializerBuilder<T> {
* <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
@@ -72,13 +72,22 @@ public interface GsonConfigSerializerBuilder<T> {
* <li>null serialization</li>
* <li>{@link Component}, {@link Style} and {@link Color} type adapters</li>
* </ul>
+ * For example, if you wanted to revert YACL's lower_camel_case naming policy,
+ * you could do the following:
+ * <pre>
+ * {@code
+ * GsonConfigSerializerBuilder.create(config)
+ * .appendGsonBuilder(builder -> builder.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY))
+ * }
+ * </pre>
*
* @param gsonBuilder the function to apply to the builder
*/
GsonConfigSerializerBuilder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder);
/**
- * Writes the json under JSON5 spec, allowing comments.
+ * Writes the json under JSON5 spec, allowing the use of {@link SerialEntry#comment()}.
+ * If enabling this option it's recommended to use the file extension {@code .json5}.
*
* @param json5 whether to write under JSON5 spec
* @return this builder
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
index dd14ed0..c363a7d 100644
--- 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
@@ -7,43 +7,50 @@ import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
import dev.isxander.yacl3.config.v2.impl.autogen.OptionFactoryRegistry;
import dev.isxander.yacl3.config.v2.impl.autogen.OptionAccessImpl;
import dev.isxander.yacl3.config.v2.impl.autogen.YACLAutoGenException;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
import dev.isxander.yacl3.platform.YACLPlatform;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.util.AbstractMap;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
+import java.util.stream.Collectors;
public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
private final Class<T> configClass;
private final ResourceLocation id;
private final boolean supportsAutoGen;
private final ConfigSerializer<T> serializer;
- private final ConfigField<?>[] fields;
+ private final ConfigFieldImpl<?>[] fields;
- private final T instance, defaults;
+ private T instance;
+ private final T defaults;
+ private final Constructor<T> noArgsConstructor;
public ConfigClassHandlerImpl(Class<T> configClass, ResourceLocation id, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) {
this.configClass = configClass;
this.id = id;
- this.supportsAutoGen = YACLPlatform.getEnvironment().isClient();
+ this.supportsAutoGen = id != null && YACLPlatform.getEnvironment().isClient();
try {
- Constructor<T> constructor = configClass.getDeclaredConstructor();
- this.instance = constructor.newInstance();
- this.defaults = constructor.newInstance();
- } catch (Exception e) {
- throw new YACLAutoGenException("Failed to create instance of config class '%s' with no-args constructor.".formatted(configClass.getName()), e);
+ noArgsConstructor = configClass.getDeclaredConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new YACLAutoGenException("Failed to find no-args constructor for config class %s.".formatted(configClass.getName()), e);
}
+ this.instance = createNewObject();
+ this.defaults = createNewObject();
this.fields = Arrays.stream(configClass.getDeclaredFields())
.peek(field -> field.setAccessible(true))
.filter(field -> field.isAnnotationPresent(SerialEntry.class) || field.isAnnotationPresent(AutoGen.class))
.map(field -> new ConfigFieldImpl<>(new ReflectionFieldAccess<>(field, instance), new ReflectionFieldAccess<>(field, defaults), this, field.getAnnotation(SerialEntry.class), field.getAnnotation(AutoGen.class)))
- .toArray(ConfigField[]::new);
+ .toArray(ConfigFieldImpl[]::new);
this.serializer = serializerFactory.apply(this);
}
@@ -63,7 +70,7 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
}
@Override
- public ConfigField<?>[] fields() {
+ public ConfigFieldImpl<?>[] fields() {
return this.fields;
}
@@ -83,6 +90,12 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
throw new YACLAutoGenException("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.");
}
+ boolean hasAutoGenFields = Arrays.stream(fields()).anyMatch(field -> field.autoGen().isPresent());
+
+ if (!hasAutoGenFields) {
+ throw new YACLAutoGenException("No fields in this config class are annotated with @AutoGen. You must annotate at least one field with @AutoGen to generate a GUI.");
+ }
+
OptionAccessImpl storage = new OptionAccessImpl();
Map<String, CategoryAndGroups> categories = new LinkedHashMap<>();
for (ConfigField<?> configField : fields()) {
@@ -134,6 +147,71 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
return this.serializer;
}
+ @Override
+ public boolean load() {
+ // create a new instance to load into
+ T newInstance = createNewObject();
+
+ // create field accesses for the new object
+ Map<ConfigFieldImpl<?>, ReflectionFieldAccess<?>> accessBufferImpl = Arrays.stream(fields())
+ .map(field -> new AbstractMap.SimpleImmutableEntry<>(
+ field,
+ new ReflectionFieldAccess<>(field.access().field(), newInstance)
+ ))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ // convert the map into API safe field accesses
+ Map<ConfigField<?>, FieldAccess<?>> accessBuffer = accessBufferImpl.entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ // attempt to load the config
+ ConfigSerializer.LoadResult loadResult = ConfigSerializer.LoadResult.FAILURE;
+ Throwable error = null;
+ try {
+ loadResult = this.serializer().loadSafely(accessBuffer);
+ } catch (Throwable e) {
+ // handle any errors later in the loadResult switch case
+ error = e;
+ }
+
+ switch (loadResult) {
+ case DIRTY:
+ case SUCCESS:
+ // replace the instance with the newly created one
+ this.instance = newInstance;
+ for (ConfigFieldImpl<?> field : fields()) {
+ // update the field accesses to point to the correct object
+ ((ConfigFieldImpl<Object>) field).setFieldAccess((ReflectionFieldAccess<Object>) accessBufferImpl.get(field));
+ }
+
+ if (loadResult == ConfigSerializer.LoadResult.DIRTY) {
+ // if the load result is dirty, we need to save the config again
+ this.save();
+ }
+ case NO_CHANGE:
+ return true;
+ case FAILURE:
+ YACLConstants.LOGGER.error(
+ "Unsuccessful load of config class '{}'. The load will be abandoned and config remains unchanged.",
+ configClass.getSimpleName(), error
+ );
+ }
+
+ return false;
+ }
+
+ @Override
+ public void save() {
+ serializer().save();
+ }
+
+ private T createNewObject() {
+ try {
+ return noArgsConstructor.newInstance();
+ } catch (Exception e) {
+ throw new YACLAutoGenException("Failed to create instance of config class '%s' with no-args constructor.".formatted(configClass.getName()), e);
+ }
+ }
+
public static class BuilderImpl<T> implements Builder<T> {
private final Class<T> configClass;
private ResourceLocation id;
@@ -157,6 +235,9 @@ public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> {
@Override
public ConfigClassHandler<T> build() {
+ Validate.notNull(serializerFactory, "serializerFactory must not be null");
+ Validate.notNull(configClass, "configClass must not be null");
+
return new ConfigClassHandlerImpl<>(configClass, id, serializerFactory);
}
}
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
index 0c879a2..aeed5ac 100644
--- 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
@@ -8,13 +8,13 @@ import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public class ConfigFieldImpl<T> implements ConfigField<T> {
- private final FieldAccess<T> field;
- private final ReadOnlyFieldAccess<T> defaultField;
+ private ReflectionFieldAccess<T> field;
+ private final ReflectionFieldAccess<T> defaultField;
private final ConfigClassHandler<?> parent;
private final Optional<SerialField> serial;
private final Optional<AutoGenField> autoGen;
- public ConfigFieldImpl(FieldAccess<T> field, ReadOnlyFieldAccess<T> defaultField, ConfigClassHandler<?> parent, @Nullable SerialEntry config, @Nullable AutoGen autoGen) {
+ public ConfigFieldImpl(ReflectionFieldAccess<T> field, ReflectionFieldAccess<T> defaultField, ConfigClassHandler<?> parent, @Nullable SerialEntry config, @Nullable AutoGen autoGen) {
this.field = field;
this.defaultField = defaultField;
this.parent = parent;
@@ -23,7 +23,9 @@ public class ConfigFieldImpl<T> implements ConfigField<T> {
? Optional.of(
new SerialFieldImpl(
"".equals(config.value()) ? field.name() : config.value(),
- "".equals(config.comment()) ? Optional.empty() : Optional.of(config.comment())
+ "".equals(config.comment()) ? Optional.empty() : Optional.of(config.comment()),
+ config.required(),
+ config.nullable()
)
)
: Optional.empty();
@@ -38,12 +40,16 @@ public class ConfigFieldImpl<T> implements ConfigField<T> {
}
@Override
- public FieldAccess<T> access() {
+ public ReflectionFieldAccess<T> access() {
return field;
}
+ public void setFieldAccess(ReflectionFieldAccess<T> field) {
+ this.field = field;
+ }
+
@Override
- public ReadOnlyFieldAccess<T> defaultAccess() {
+ public ReflectionFieldAccess<T> defaultAccess() {
return defaultField;
}
@@ -62,7 +68,7 @@ public class ConfigFieldImpl<T> implements ConfigField<T> {
return this.autoGen;
}
- private record SerialFieldImpl(String serialName, Optional<String> comment) implements SerialField {
+ private record SerialFieldImpl(String serialName, Optional<String> comment, boolean required, boolean nullable) implements SerialField {
}
private record AutoGenFieldImpl<T>(String category, Optional<String> group) implements AutoGenField {
}
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
index d308c23..a60bcb1 100644
--- 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
@@ -11,6 +11,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.world.item.Item;
+import org.jetbrains.annotations.ApiStatus;
import org.quiltmc.parsers.json.JsonReader;
import org.quiltmc.parsers.json.JsonWriter;
import org.quiltmc.parsers.json.gson.GsonReader;
@@ -25,6 +26,7 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@@ -48,12 +50,12 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
try (StringWriter stringWriter = new StringWriter()) {
JsonWriter jsonWriter = json5 ? JsonWriter.json5(stringWriter) : JsonWriter.json(stringWriter);
GsonWriter gsonWriter = new GsonWriter(jsonWriter);
+
jsonWriter.beginObject();
+
for (ConfigField<?> field : config.fields()) {
SerialField serial = field.serial().orElse(null);
- if (serial == null) {
- continue;
- }
+ if (serial == null) continue;
if (!json5 && serial.comment().isPresent() && YACLPlatform.isDevelopmentEnv()) {
YACLConstants.LOGGER.warn("Found comment in config field '{}', but json5 is not enabled. Enable it with `.setJson5(true)` on the `GsonConfigSerializerBuilder`. Comments will not be serialized. This warning is only visible in development environments.", serial.serialName());
@@ -61,13 +63,24 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
jsonWriter.comment(serial.comment().orElse(null));
jsonWriter.name(serial.serialName());
+
+ JsonElement element;
try {
- gson.toJson(field.access().get(), field.access().type(), gsonWriter);
+ element = gson.toJsonTree(field.access().get(), field.access().type());
} catch (Exception e) {
- YACLConstants.LOGGER.error("Failed to serialize config field '{}'.", serial.serialName(), e);
+ YACLConstants.LOGGER.error("Failed to serialize config field '{}'. Serializing as null.", serial.serialName(), e);
jsonWriter.nullValue();
+ continue;
+ }
+
+ try {
+ gson.toJson(element, gsonWriter);
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to serialize config field '{}'. Due to the error state this JSON writer cannot continue safely and the save will be abandoned.", serial.serialName(), e);
+ return;
}
}
+
jsonWriter.endObject();
jsonWriter.flush();
@@ -79,46 +92,85 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
}
@Override
- public void load() {
+ public LoadResult loadSafely(Map<ConfigField<?>, FieldAccess<?>> bufferAccessMap) {
if (!Files.exists(path)) {
YACLConstants.LOGGER.info("Config file '{}' does not exist. Creating it with default values.", path);
save();
- return;
+ return LoadResult.NO_CHANGE;
}
YACLConstants.LOGGER.info("Deserializing {} from '{}'", config.configClass().getSimpleName(), path);
+ Map<String, ConfigField<?>> fieldMap = Arrays.stream(config.fields())
+ .filter(field -> field.serial().isPresent())
+ .collect(Collectors.toMap(f -> f.serial().orElseThrow().serialName(), Function.identity()));
+ Set<String> missingFields = fieldMap.keySet();
+ boolean dirty = false;
+
try (JsonReader jsonReader = json5 ? JsonReader.json5(path) : JsonReader.json(path)) {
GsonReader gsonReader = new GsonReader(jsonReader);
- Map<String, ConfigField<?>> fieldMap = Arrays.stream(config.fields())
- .filter(field -> field.serial().isPresent())
- .collect(Collectors.toMap(f -> f.serial().orElseThrow().serialName(), Function.identity()));
-
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
ConfigField<?> field = fieldMap.get(name);
+ missingFields.remove(name);
+
if (field == null) {
- YACLConstants.LOGGER.warn("Found unknown config field '{}' in '{}'.", name, path);
+ YACLConstants.LOGGER.warn("Found unknown config field '{}'.", name);
jsonReader.skipValue();
continue;
}
+ FieldAccess<?> bufferAccess = bufferAccessMap.get(field);
+ SerialField serial = field.serial().orElse(null);
+ if (serial == null) continue;
+
+ JsonElement element;
try {
- field.access().set(gson.fromJson(gsonReader, field.access().type()));
+ element = gson.fromJson(gsonReader, JsonElement.class);
} catch (Exception e) {
- YACLConstants.LOGGER.error("Failed to deserialize config field '{}'.", name, e);
- jsonReader.skipValue();
+ YACLConstants.LOGGER.error("Failed to deserialize config field '{}'. Due to the error state this JSON reader cannot be re-used and loading will be aborted.", name, e);
+ return LoadResult.FAILURE;
+ }
+
+ if (element.isJsonNull() && !serial.nullable()) {
+ YACLConstants.LOGGER.warn("Found null value in non-nullable config field '{}'. Leaving field as default and marking as dirty.", name);
+ dirty = true;
+ continue;
+ }
+
+ try {
+ bufferAccess.set(gson.fromJson(element, bufferAccess.type()));
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to deserialize config field '{}'. Leaving as default.", name, e);
}
}
jsonReader.endObject();
} catch (IOException e) {
- YACLConstants.LOGGER.error("Failed to deserialize config class '{}'.", config.configClass().getSimpleName(), e);
+ YACLConstants.LOGGER.error("Failed to deserialize config class.", e);
+ return LoadResult.FAILURE;
}
+ if (!missingFields.isEmpty()) {
+ for (String missingField : missingFields) {
+ if (fieldMap.get(missingField).serial().orElseThrow().required()) {
+ dirty = true;
+ YACLConstants.LOGGER.warn("Missing required config field '{}''. Re-saving as default.", missingField);
+ }
+ }
+ }
+
+ return dirty ? LoadResult.DIRTY : LoadResult.SUCCESS;
+ }
+
+ @Override
+ @Deprecated
+ public void load() {
+ YACLConstants.LOGGER.warn("Calling ConfigSerializer#load() directly is deprecated. Please use ConfigClassHandler#load() instead.");
+ config.load();
}
public static class ColorTypeAdapter implements JsonSerializer<Color>, JsonDeserializer<Color> {
@@ -145,6 +197,7 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
}
}
+ @ApiStatus.Internal
public static class Builder<T> implements GsonConfigSerializerBuilder<T> {
private final ConfigClassHandler<T> config;
private Path path;
diff --git a/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java b/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java
index c8981d4..31942be 100644
--- a/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java
+++ b/test-common/src/main/java/dev/isxander/yacl3/test/GuiTest.java
@@ -47,6 +47,13 @@ public class GuiTest {
Minecraft.getInstance().setScreen(AutogenConfigTest.INSTANCE.generateGui().generateScreen(screen));
})
.build())
+ .option(ButtonOption.createBuilder()
+ .name(Component.literal("Skyblocker test"))
+ .action((screen, opt) -> {
+ SkyblockerConfig.HANDLER.serializer().load();
+ Minecraft.getInstance().setScreen(SkyblockerConfig.HANDLER.generateGui().generateScreen(screen));
+ })
+ .build())
.group(OptionGroup.createBuilder()
.name(Component.literal("Wiki"))
.option(ButtonOption.createBuilder()