From a3cb1c28fb7d8cd8c22f528aae7d6ca4ba523513 Mon Sep 17 00:00:00 2001 From: Aaron <51387595+AzureAaron@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:41:54 -0400 Subject: Use custom vanilla GSON config serialization The mysterious config wipe errors were the result of malformed json, normal GSON is extensively tested and is highly unlikely to produce that itself but YACL uses an intermediary parser that has had similar problems with this stuff in the past. By circumventing that library, this issue can hopefully be fixed. --- .../hysky/skyblocker/config/CodecTypeAdapter.java | 28 -------- .../skyblocker/config/SkyblockerConfigManager.java | 25 +++++-- .../config/serialization/CodecTypeAdapter.java | 28 ++++++++ .../config/serialization/ItemTypeAdapter.java | 29 ++++++++ .../serialization/VanillaGsonConfigSerializer.java | 84 ++++++++++++++++++++++ .../accessors/ConfigClassHandlerImplAccessor.java | 13 ++++ .../java/de/hysky/skyblocker/utils/CodecUtils.java | 2 + 7 files changed, 174 insertions(+), 35 deletions(-) delete mode 100644 src/main/java/de/hysky/skyblocker/config/CodecTypeAdapter.java create mode 100644 src/main/java/de/hysky/skyblocker/config/serialization/CodecTypeAdapter.java create mode 100644 src/main/java/de/hysky/skyblocker/config/serialization/ItemTypeAdapter.java create mode 100644 src/main/java/de/hysky/skyblocker/config/serialization/VanillaGsonConfigSerializer.java create mode 100644 src/main/java/de/hysky/skyblocker/mixins/accessors/ConfigClassHandlerImplAccessor.java (limited to 'src/main/java/de') diff --git a/src/main/java/de/hysky/skyblocker/config/CodecTypeAdapter.java b/src/main/java/de/hysky/skyblocker/config/CodecTypeAdapter.java deleted file mode 100644 index 27544703..00000000 --- a/src/main/java/de/hysky/skyblocker/config/CodecTypeAdapter.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.hysky.skyblocker.config; - -import java.lang.reflect.Type; - -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.mojang.serialization.Codec; -import com.mojang.serialization.JsonOps; - -/** - * Creates a GSON type adapter from a {@link Codec}. - */ -public record CodecTypeAdapter(Codec codec) implements JsonSerializer, JsonDeserializer { - - @Override - public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return codec.parse(JsonOps.INSTANCE, json).getOrThrow(JsonParseException::new); - } - - @Override - public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { - return codec.encodeStart(JsonOps.INSTANCE, src).getOrThrow(); - } -} diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java index 33c92959..6449e770 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java @@ -4,12 +4,15 @@ import com.google.gson.FieldNamingPolicy; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.categories.*; +import de.hysky.skyblocker.config.serialization.CodecTypeAdapter; +import de.hysky.skyblocker.config.serialization.ItemTypeAdapter; +import de.hysky.skyblocker.config.serialization.VanillaGsonConfigSerializer; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; +import de.hysky.skyblocker.utils.CodecUtils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import dev.isxander.yacl3.api.YetAnotherConfigLib; import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; -import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -20,9 +23,13 @@ import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.item.Item; +import net.minecraft.text.Style; import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; import net.minecraft.util.Identifier; +import java.awt.Color; import java.lang.StackWalker.Option; import java.nio.file.Path; import java.util.function.Consumer; @@ -33,13 +40,17 @@ public class SkyblockerConfigManager { public static final int CONFIG_VERSION = 4; private static final Path CONFIG_FILE = FabricLoader.getInstance().getConfigDir().resolve("skyblocker.json"); private static final ConfigClassHandler HANDLER = ConfigClassHandler.createBuilder(SkyblockerConfig.class) - .serializer(config -> GsonConfigSerializerBuilder.create(config) - .setPath(CONFIG_FILE) - .setJson5(false) - .appendGsonBuilder(builder -> builder + .serializer(config -> new VanillaGsonConfigSerializer<>(config, CONFIG_FILE, builder -> builder .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) - .registerTypeHierarchyAdapter(Identifier.class, new CodecTypeAdapter<>(Identifier.CODEC))) - .build()) + .setPrettyPrinting() + .serializeNulls() + .registerTypeHierarchyAdapter(Color.class, new CodecTypeAdapter<>(CodecUtils.COLOR_CODEC)) + .registerTypeHierarchyAdapter(Text.class, new CodecTypeAdapter<>(TextCodecs.CODEC)) + .registerTypeHierarchyAdapter(Style.class, new CodecTypeAdapter<>(Style.Codecs.CODEC)) + .registerTypeHierarchyAdapter(Identifier.class, new CodecTypeAdapter<>(Identifier.CODEC)) + .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter()) + ) + ) .build(); public static SkyblockerConfig get() { diff --git a/src/main/java/de/hysky/skyblocker/config/serialization/CodecTypeAdapter.java b/src/main/java/de/hysky/skyblocker/config/serialization/CodecTypeAdapter.java new file mode 100644 index 00000000..26b04017 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/serialization/CodecTypeAdapter.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.config.serialization; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + +/** + * Creates a GSON type adapter from a {@link Codec}. + */ +public record CodecTypeAdapter(Codec codec) implements JsonSerializer, JsonDeserializer { + + @Override + public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return codec.parse(JsonOps.INSTANCE, json).getOrThrow(JsonParseException::new); + } + + @Override + public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) { + return codec.encodeStart(JsonOps.INSTANCE, src).getOrThrow(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/serialization/ItemTypeAdapter.java b/src/main/java/de/hysky/skyblocker/config/serialization/ItemTypeAdapter.java new file mode 100644 index 00000000..d854b1b5 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/serialization/ItemTypeAdapter.java @@ -0,0 +1,29 @@ +package de.hysky.skyblocker.config.serialization; + +import java.lang.reflect.Type; +import java.util.Locale; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import net.minecraft.item.Item; +import net.minecraft.registry.Registries; +import net.minecraft.util.Identifier; + +public class ItemTypeAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public Item deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Registries.ITEM.get(Identifier.of(json.getAsString().toLowerCase(Locale.ENGLISH))); + } + + @Override + public JsonElement serialize(Item src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(Registries.ITEM.getId(src).toString()); + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/serialization/VanillaGsonConfigSerializer.java b/src/main/java/de/hysky/skyblocker/config/serialization/VanillaGsonConfigSerializer.java new file mode 100644 index 00000000..a9640fd3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/serialization/VanillaGsonConfigSerializer.java @@ -0,0 +1,84 @@ +package de.hysky.skyblocker.config.serialization; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; +import java.util.function.UnaryOperator; + +import org.slf4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; + +import de.hysky.skyblocker.mixins.accessors.ConfigClassHandlerImplAccessor; +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.FieldAccess; + +public class VanillaGsonConfigSerializer extends ConfigSerializer { + private static final Logger LOGGER = LogUtils.getLogger(); + private final Path path; + private final Gson gson; + + public VanillaGsonConfigSerializer(ConfigClassHandler config, Path path, UnaryOperator builderUpdater) { + super(config); + this.path = path; + this.gson = builderUpdater.apply(new GsonBuilder()).create(); + } + + @Override + public void save() { + T instance = this.config.instance(); + + try { + String json = this.gson.toJson(instance); + + Files.createDirectories(path.getParent()); + Files.writeString(this.path, json, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + LOGGER.info("[Skyblocker Config] Successfully saved config file to {}.", this.path); + } catch (Exception e) { + LOGGER.error(LogUtils.FATAL_MARKER, "[Skyblocker Config] Failed to save the config file to: {}!!!", this.path, e); + } + } + + @Override + public LoadResult loadSafely(Map, FieldAccess> bufferAccessMap) { + try { + if (!Files.exists(this.path)) { + ((ConfigClassHandlerImplAccessor) this.config).setInstance(createNewConfigInstance()); + save(); + return LoadResult.NO_CHANGE; + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Config] Failed to create default config file!", e); + } + + try { + String config = Files.readString(this.path); + T instance = this.gson.fromJson(JsonParser.parseString(config), this.config.configClass()); + + //Set the instance + ((ConfigClassHandlerImplAccessor) this.config).setInstance(instance); + LOGGER.info("[Skyblocker Config] Successfully loaded the config file from {}.", this.path); + } catch (Exception e) { + LOGGER.error(LogUtils.FATAL_MARKER, "[Skyblocker Config] Failed to load the config!", e); + } + + //We use no change to avoid the instance being set by YACL to a fresh one since we just did that ourselves. + return LoadResult.NO_CHANGE; + } + + private T createNewConfigInstance() { + try { + return (T) this.config.configClass().getDeclaredConstructor().newInstance(); + } catch (Exception e) { + LOGGER.error("[Skyblocker Config] Failed to create new config instance!", e); + } + + return null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/ConfigClassHandlerImplAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/ConfigClassHandlerImplAccessor.java new file mode 100644 index 00000000..cc9c2727 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/ConfigClassHandlerImplAccessor.java @@ -0,0 +1,13 @@ +package de.hysky.skyblocker.mixins.accessors; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import dev.isxander.yacl3.config.v2.impl.ConfigClassHandlerImpl; + +@Mixin(ConfigClassHandlerImpl.class) +public interface ConfigClassHandlerImplAccessor { + + @Accessor + void setInstance(Object instance); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/CodecUtils.java b/src/main/java/de/hysky/skyblocker/utils/CodecUtils.java index 649f2dc4..2c01a3ec 100644 --- a/src/main/java/de/hysky/skyblocker/utils/CodecUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/CodecUtils.java @@ -4,10 +4,12 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import it.unimi.dsi.fastutil.objects.*; +import java.awt.Color; import java.util.*; import java.util.function.Function; public final class CodecUtils { + public static final Codec COLOR_CODEC = Codec.INT.xmap(argb -> new Color(argb, true), color -> color.getRGB()); private CodecUtils() { throw new IllegalStateException("Uhhhh no? like just no. What are you trying to do? D- Do you think this will be useful to instantiate this? Like it's private, so you went through the effort of putting an accessor actually i'm not sure you can accessor a constructor. can you? so if not did you really put an access widener for that? like really? honestly this is just sad. Plus there aren't even any method in here that requires an instance. There's only static methods. like bruh. you know what i'm done typing shit for you to read, bye i'm leaving *voice lowers as I leave* I swear those modders think they can access all they want sheesh *comes back instantly* AND I SWEAR IF YOU INJECT SO THIS ERROR CANNOT BE THROWN I WILL SEND YOU TO HELL'S FREEZER"); -- cgit