From c6323e97596b303d1b6384ac93967831c59a34f2 Mon Sep 17 00:00:00 2001
From: AnimeGitB <AnimeGitB@bigblueball.in>
Date: Tue, 9 Aug 2022 21:44:44 +0930
Subject: [PATCH] Funnel all gson calls into helper functions

Add deprecated getGsonFactory for plugin compat until 3.0
---
 .../java/emu/grasscutter/Grasscutter.java     |  11 +-
 .../grasscutter/config/ConfigContainer.java   |   6 +-
 .../java/emu/grasscutter/data/DataLoader.java |  19 +++
 .../emu/grasscutter/data/ResourceLoader.java  | 153 ++++++++----------
 .../game/activity/ActivityManager.java        |  13 +-
 .../game/activity/PlayerActivityData.java     |   3 +-
 .../musicgame/MusicGameActivityHandler.java   |   4 +-
 .../game/combine/CombineManger.java           |  17 +-
 .../emu/grasscutter/game/drop/DropSystem.java |  10 +-
 .../dungeons/challenge/DungeonChallenge.java  |  14 +-
 .../game/expedition/ExpeditionSystem.java     |  13 +-
 .../grasscutter/game/gacha/GachaSystem.java   |  13 +-
 .../game/managers/energy/EnergyManager.java   |  20 +--
 .../emu/grasscutter/game/shop/ShopSystem.java |  23 ++-
 .../game/systems/AnnouncementSystem.java      |   7 +-
 .../grasscutter/game/tower/TowerSystem.java   |   9 +-
 .../game/world/WorldDataSystem.java           |  18 +--
 .../net/packet/PacketOpcodesUtils.java        |   3 +-
 .../emu/grasscutter/plugin/PluginManager.java |   2 +-
 .../java/emu/grasscutter/utils/Language.java  |   2 +-
 .../java/emu/grasscutter/utils/Utils.java     |  56 ++++++-
 21 files changed, 195 insertions(+), 221 deletions(-)

diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java
index d7d64fb6..0f3827ab 100644
--- a/src/main/java/emu/grasscutter/Grasscutter.java
+++ b/src/main/java/emu/grasscutter/Grasscutter.java
@@ -3,7 +3,6 @@ package emu.grasscutter;
 import ch.qos.logback.classic.Level;
 import ch.qos.logback.classic.Logger;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 
 import emu.grasscutter.auth.AuthenticationSystem;
 import emu.grasscutter.auth.DefaultAuthentication;
@@ -56,7 +55,6 @@ public final class Grasscutter {
 
     private static Language language;
 
-    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
     public static final File configFile = new File("./config.json");
 
     private static int day; // Current day of week.
@@ -203,8 +201,8 @@ public final class Grasscutter {
         }
 
         // If the file already exists, we attempt to load it.
-        try (FileReader file = new FileReader(configFile)) {
-            config = gson.fromJson(file, ConfigContainer.class);
+        try {
+            config = Utils.loadJsonToClass(configFile.getPath(), ConfigContainer.class);
         } catch (Exception exception) {
             getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
             System.exit(1);
@@ -220,7 +218,7 @@ public final class Grasscutter {
         if (config == null) config = new ConfigContainer();
 
         try (FileWriter file = new FileWriter(configFile)) {
-            file.write(gson.toJson(config));
+            file.write(Utils.jsonEncode(config));
         } catch (IOException ignored) {
             Grasscutter.getLogger().error("Unable to write to config file.");
         } catch (Exception e) {
@@ -272,8 +270,9 @@ public final class Grasscutter {
         return consoleLineReader;
     }
 
+    @Deprecated(forRemoval = true)
     public static Gson getGsonFactory() {
-        return gson;
+        return Utils.getGsonFactory();
     }
 
     public static HttpServer getHttpServer() {
diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java
index 5d94c14b..f6e84a7d 100644
--- a/src/main/java/emu/grasscutter/config/ConfigContainer.java
+++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java
@@ -4,10 +4,9 @@ import com.google.gson.JsonObject;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.Grasscutter.ServerDebugMode;
 import emu.grasscutter.Grasscutter.ServerRunMode;
+import emu.grasscutter.utils.Utils;
 
-import java.nio.charset.StandardCharsets;
 import java.util.Set;
-import java.io.FileReader;
 import java.lang.reflect.Field;
 import java.util.Arrays;
 import java.util.Locale;
@@ -27,8 +26,7 @@ public class ConfigContainer {
      */
     public static void updateConfig() {
         try { // Check if the server is using a legacy config.
-            JsonObject configObject = Grasscutter.getGsonFactory()
-                    .fromJson(new FileReader(Grasscutter.configFile, StandardCharsets.UTF_8), JsonObject.class);
+            JsonObject configObject = Utils.loadJsonToClass(Grasscutter.configFile.getPath(), JsonObject.class);
             if (!configObject.has("version")) {
                 Grasscutter.getLogger().info("Updating legacy ..");
                 Grasscutter.saveConfig(null);
diff --git a/src/main/java/emu/grasscutter/data/DataLoader.java b/src/main/java/emu/grasscutter/data/DataLoader.java
index 90299659..d7f1a7c7 100644
--- a/src/main/java/emu/grasscutter/data/DataLoader.java
+++ b/src/main/java/emu/grasscutter/data/DataLoader.java
@@ -15,6 +15,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.Map;
 
 public class DataLoader {
 
@@ -69,6 +70,24 @@ public class DataLoader {
         return null;
     }
 
+    public static <T> T loadClass(String resourcePath, Class<T> classType) throws IOException {
+        try (InputStreamReader reader = loadReader(resourcePath)) {
+            return Utils.loadJsonToClass(reader, classType);
+        }
+    }
+
+    public static <T> List<T> loadList(String resourcePath, Class<T> classType) throws IOException {
+        try (InputStreamReader reader = loadReader(resourcePath)) {
+            return Utils.loadJsonToList(reader, classType);
+        }
+    }
+
+    public static <T1,T2> Map<T1,T2> loadMap(String resourcePath, Class<T1> keyType, Class<T2> valueType) throws IOException {
+        try (InputStreamReader reader = loadReader(resourcePath)) {
+            return Utils.loadJsonToMap(reader, keyType, valueType);
+        }
+    }
+
     public static void checkAllFiles() {
         try {
             List<Path> filenames = FileUtils.getPathsFromResource("/defaults/data/");
diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java
index 5ceb756f..56aa89df 100644
--- a/src/main/java/emu/grasscutter/data/ResourceLoader.java
+++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java
@@ -2,7 +2,6 @@ package emu.grasscutter.data;
 
 import com.google.gson.JsonElement;
 import com.google.gson.annotations.SerializedName;
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.binout.*;
 import emu.grasscutter.data.binout.AbilityModifier.AbilityConfigData;
@@ -21,7 +20,6 @@ import lombok.SneakyThrows;
 import org.reflections.Reflections;
 
 import java.io.*;
-import java.lang.reflect.Type;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.*;
@@ -29,6 +27,7 @@ import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static emu.grasscutter.config.Configuration.DATA;
 import static emu.grasscutter.config.Configuration.RESOURCE;
 import static emu.grasscutter.utils.Language.translate;
 
@@ -119,15 +118,13 @@ public class ResourceLoader {
     }
 
     @SuppressWarnings({"rawtypes", "unchecked"})
-    protected static void loadFromResource(Class<?> c, String fileName, Int2ObjectMap map) throws Exception {
-        try (FileReader fileReader = new FileReader(RESOURCE("ExcelBinOutput/" + fileName))) {
-            List list = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, c).getType());
-
-            for (Object o : list) {
-                GameResource res = (GameResource) o;
-                res.onLoad();
-                map.put(res.getId(), res);
-            }
+    protected static <T> void loadFromResource(Class<T> c, String fileName, Int2ObjectMap map) throws Exception {
+        List<T> list = Utils.loadJsonToList(RESOURCE("ExcelBinOutput/" + fileName), c);
+
+        for (T o : list) {
+            GameResource res = (GameResource) o;
+            res.onLoad();
+            map.put(res.getId(), res);
         }
     }
 
@@ -140,7 +137,6 @@ public class ResourceLoader {
             return;
         }
 
-        List<ScenePointEntry> scenePointList = new ArrayList<>();
         for (File file : Objects.requireNonNull(folder.listFiles())) {
             ScenePointConfig config; 
             Integer sceneId;
@@ -152,8 +148,8 @@ public class ResourceLoader {
                 continue;
             }
 
-            try (FileReader fileReader = new FileReader(file)) {
-                config = Grasscutter.getGsonFactory().fromJson(fileReader, ScenePointConfig.class);
+            try {
+                config = Utils.loadJsonToClass(file.getPath(), ScenePointConfig.class);
             } catch (Exception e) {
                 e.printStackTrace();
                 continue;
@@ -163,22 +159,20 @@ public class ResourceLoader {
                 continue;
             }
 
+            List<Integer> scenePoints = new ArrayList<>();
             for (Map.Entry<String, JsonElement> entry : config.points.entrySet()) {
-                PointData pointData = Grasscutter.getGsonFactory().fromJson(entry.getValue(), PointData.class);
-                pointData.setId(Integer.parseInt(entry.getKey()));
+                int id = Integer.parseInt(entry.getKey());
+                String name = sceneId + "_" + entry.getKey();
+                PointData pointData = Utils.jsonDecode(entry.getValue(), PointData.class);
+                pointData.setId(id);
 
-                ScenePointEntry sl = new ScenePointEntry(sceneId + "_" + entry.getKey(), pointData);
-                scenePointList.add(sl);
-                GameData.getScenePointIdList().add(pointData.getId());
+                GameData.getScenePointIdList().add(id);
+                GameData.getScenePointEntries().put(name, new ScenePointEntry(name, pointData));
+                scenePoints.add(id);
 
                 pointData.updateDailyDungeon();
             }
-
-            GameData.getScenePointsPerScene().put(sceneId, new ArrayList<>());
-            for (ScenePointEntry entry : scenePointList) {
-                GameData.getScenePointEntries().put(entry.getName(), entry);
-                GameData.getScenePointsPerScene().get(sceneId).add(entry.getPointData().getId());
-            }
+            GameData.getScenePointsPerScene().put(sceneId, scenePoints);
         }
     }
 
@@ -186,15 +180,15 @@ public class ResourceLoader {
         List<AbilityEmbryoEntry> embryoList = null;
 
         // Read from cached file if exists
-        try (InputStream embryoCache = DataLoader.load("AbilityEmbryos.json", false)) {
-            embryoList = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(embryoCache), TypeToken.getParameterized(Collection.class, AbilityEmbryoEntry.class).getType());
+        try {
+            embryoList = Utils.loadJsonToList(DATA("AbilityEmbryos.json"), AbilityEmbryoEntry.class);
         } catch (Exception ignored) {}
 
         if (embryoList == null) {
             // Load from BinOutput
             Pattern pattern = Pattern.compile("(?<=ConfigAvatar_)(.*?)(?=.json)");
 
-            embryoList = new LinkedList<>();
+            embryoList = new ArrayList<>();
             File folder = new File(Utils.toFilePath(RESOURCE("BinOutput/Avatar/")));
             File[] files = folder.listFiles();
             if (files == null) {
@@ -213,8 +207,8 @@ public class ResourceLoader {
                     continue;
                 }
 
-                try (FileReader fileReader = new FileReader(file)) {
-                    config = Grasscutter.getGsonFactory().fromJson(fileReader, AvatarConfig.class);
+                try {
+                    config = Utils.loadJsonToClass(file.getPath(), AvatarConfig.class);
                 } catch (Exception e) {
                     e.printStackTrace();
                     continue;
@@ -229,14 +223,10 @@ public class ResourceLoader {
                 embryoList.add(al);
             }
 
-            File playerElementsFile = new File(Utils.toFilePath(RESOURCE("BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json")));
-
-            if (playerElementsFile.exists()) {
-                try (FileReader fileReader = new FileReader(playerElementsFile)) {
-                    GameDepot.setPlayerAbilities(Grasscutter.getGsonFactory().fromJson(fileReader, new TypeToken<Map<String, AvatarConfig>>(){}.getType()));
-                } catch (Exception e) {
-                    e.printStackTrace();
-                }
+            try {
+                GameDepot.setPlayerAbilities(Utils.loadJsonToMap(RESOURCE("BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), String.class, AvatarConfig.class));
+            } catch (Exception e) {
+                e.printStackTrace();
             }
         }
 
@@ -262,8 +252,8 @@ public class ResourceLoader {
         for (File file : files) {
             List<AbilityConfigData> abilityConfigList;
 
-            try (FileReader fileReader = new FileReader(file)) {
-                abilityConfigList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, AbilityConfigData.class).getType());
+            try {
+                abilityConfigList = Utils.loadJsonToList(file.getPath(), AbilityConfigData.class);
             } catch (Exception e) {
                 e.printStackTrace();
                 continue;
@@ -320,11 +310,8 @@ public class ResourceLoader {
         for (String name : spawnDataNames) {
             // Load spawn entries from file
             try (InputStreamReader reader = DataLoader.loadReader(name)) {
-                Type type = TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType();
-                List<SpawnGroupEntry> list = Grasscutter.getGsonFactory().fromJson(reader, type);
-
                 // Add spawns to group if it already exists in our spawn group map
-                spawnEntryMap.addAll(list);
+                spawnEntryMap.addAll(Utils.loadJsonToList(reader, SpawnGroupEntry.class));
             } catch (Exception ignored) {}
         }
 
@@ -354,13 +341,12 @@ public class ResourceLoader {
         // Read from cached file if exists
         List<OpenConfigEntry> list = null;
 
-        try (InputStream openConfigCache = DataLoader.load("OpenConfig.json", false)) {
-            list = Grasscutter.getGsonFactory().fromJson(new InputStreamReader(openConfigCache), TypeToken.getParameterized(Collection.class, SpawnGroupEntry.class).getType());
+        try {
+            list = Utils.loadJsonToList(DATA("OpenConfig.json"), OpenConfigEntry.class);
         } catch (Exception ignored) {}
 
         if (list == null) {
             Map<String, OpenConfigEntry> map = new TreeMap<>();
-            java.lang.reflect.Type type = new TypeToken<Map<String, OpenConfigData[]>>() {}.getType();
             String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"};
 
             for (String name : folderNames) {
@@ -374,11 +360,10 @@ public class ResourceLoader {
                     if (!file.getName().endsWith(".json")) {
                         continue;
                     }
-
                     Map<String, OpenConfigData[]> config;
 
-                    try (FileReader fileReader = new FileReader(file)) {
-                        config = Grasscutter.getGsonFactory().fromJson(fileReader, type);
+                    try {
+                        config = Utils.loadJsonToMap(file.getPath(), String.class, OpenConfigData[].class);
                     } catch (Exception e) {
                         e.printStackTrace();
                         continue;
@@ -414,8 +399,8 @@ public class ResourceLoader {
         for (File file : folder.listFiles()) {
             MainQuestData mainQuest = null;
 
-            try (FileReader fileReader = new FileReader(file)) {
-                mainQuest = Grasscutter.getGsonFactory().fromJson(fileReader, MainQuestData.class);
+            try {
+                mainQuest = Utils.loadJsonToClass(file.getPath(), MainQuestData.class);
             } catch (Exception e) {
                 e.printStackTrace();
                 continue;
@@ -424,16 +409,14 @@ public class ResourceLoader {
             GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest);
         }
 
-        try (Reader reader = new FileReader(RESOURCE("QuestEncryptionKeys.json"))) {
-            List<QuestEncryptionKey> keys = Grasscutter.getGsonFactory().fromJson(
-                reader,
-                TypeToken.getParameterized(List.class, QuestEncryptionKey.class).getType());
+        try {
+            List<QuestEncryptionKey> keys = DataLoader.loadList("QuestEncryptionKeys.json", QuestEncryptionKey.class);
 
             Int2ObjectMap<QuestEncryptionKey> questEncryptionMap = GameData.getMainQuestEncryptionMap();
             keys.forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
             Grasscutter.getLogger().debug("Loaded {} quest keys.", questEncryptionMap.size());
-        } catch (FileNotFoundException ignored) {
-            Grasscutter.getLogger().error("Unable to load quest keys - ./resources/QuestEncryptionKeys.json not found.");
+        } catch (FileNotFoundException | NullPointerException ignored) {
+            Grasscutter.getLogger().warn("Unable to load quest keys - ./resources/QuestEncryptionKeys.json not found.");
         } catch (Exception e) {
             Grasscutter.getLogger().error("Unable to load quest keys.", e);
         }
@@ -450,8 +433,8 @@ public class ResourceLoader {
 
         for (File file : folder.listFiles()) {
             ScriptSceneData sceneData;
-            try (FileReader fileReader = new FileReader(file)) {
-                sceneData = Grasscutter.getGsonFactory().fromJson(fileReader, ScriptSceneData.class);
+            try {
+                sceneData = Utils.loadJsonToClass(file.getPath(), ScriptSceneData.class);
             } catch (Exception e) {
                 e.printStackTrace();
                 continue;
@@ -463,43 +446,41 @@ public class ResourceLoader {
         Grasscutter.getLogger().debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas.");
     }
 
-	@SneakyThrows
-	private static void loadHomeworldDefaultSaveData(){
-		var folder = Files.list(Path.of(RESOURCE("BinOutput/HomeworldDefaultSave"))).toList();
-		var pattern = Pattern.compile("scene(.*)_home_config.json");
-
-        for (var file : folder) {
-            var matcher = pattern.matcher(file.getFileName().toString());
+    @SneakyThrows
+    private static void loadHomeworldDefaultSaveData() {
+        var pattern = Pattern.compile("scene(.*)_home_config.json");
+        Files.list(Path.of(RESOURCE("BinOutput/HomeworldDefaultSave"))).forEach(file -> {
+            String filename = file.getFileName().toString();
+            var matcher = pattern.matcher(filename);
             if (!matcher.find()) {
-                continue;
+                return;
             }
-            var sceneId = matcher.group(1);
-
-            var data = Grasscutter.getGsonFactory().fromJson(Files.readString(file), HomeworldDefaultSaveData.class);
-
-            GameData.getHomeworldDefaultSaveData().put(Integer.parseInt(sceneId), data);
-        }
+            try {
+                var sceneId = Integer.parseInt(matcher.group(1));
+                var data = Utils.loadJsonToClass(filename, HomeworldDefaultSaveData.class);
+                GameData.getHomeworldDefaultSaveData().put(sceneId, data);
+            } catch (Exception ignored) {}
+        });
 
         Grasscutter.getLogger().debug("Loaded " + GameData.getHomeworldDefaultSaveData().size() + " HomeworldDefaultSaveDatas.");
     }
 
     @SneakyThrows
     private static void loadNpcBornData() {
-        var folder = Files.list(Path.of(RESOURCE("BinOutput/Scene/SceneNpcBorn"))).toList();
-
-        for (var file : folder) {
+        Files.list(Path.of(RESOURCE("BinOutput/Scene/SceneNpcBorn"))).forEach(file -> {
             if (file.toFile().isDirectory()) {
-                continue;
-            }
-
-            var data = Grasscutter.getGsonFactory().fromJson(Files.readString(file), SceneNpcBornData.class);
-            if (data.getBornPosList() == null || data.getBornPosList().size() == 0) {
-                continue;
+                return;
             }
+            try {
+                var data = Utils.loadJsonToClass(file.getFileName().toString(), SceneNpcBornData.class);
+                if (data.getBornPosList() == null || data.getBornPosList().size() == 0) {
+                    return;
+                }
 
-            data.setIndex(SceneIndexManager.buildIndex(3, data.getBornPosList(), item -> item.getPos().toPoint()));
-            GameData.getSceneNpcBornData().put(data.getSceneId(), data);
-        }
+                data.setIndex(SceneIndexManager.buildIndex(3, data.getBornPosList(), item -> item.getPos().toPoint()));
+                GameData.getSceneNpcBornData().put(data.getSceneId(), data);
+            } catch (Exception ignored) {}
+        });
 
         Grasscutter.getLogger().debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas.");
     }
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityManager.java b/src/main/java/emu/grasscutter/game/activity/ActivityManager.java
index f4cd55f0..a859d848 100644
--- a/src/main/java/emu/grasscutter/game/activity/ActivityManager.java
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityManager.java
@@ -1,7 +1,6 @@
 package emu.grasscutter.game.activity;
 
 import com.esotericsoftware.reflectasm.ConstructorAccess;
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.data.GameData;
@@ -14,9 +13,6 @@ import emu.grasscutter.server.packet.send.PacketActivityScheduleInfoNotify;
 import lombok.Getter;
 import org.reflections.Reflections;
 
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -45,13 +41,8 @@ public class ActivityManager extends BasePlayerManager {
             activityWatcherTypeMap.put(typeName.value(), ConstructorAccess.get(item));
         });
 
-        try (Reader reader = DataLoader.loadReader("ActivityConfig.json")) {
-            List<ActivityConfigItem> activities = Grasscutter.getGsonFactory().fromJson(
-                reader,
-                TypeToken.getParameterized(List.class, ActivityConfigItem.class).getType());
-
-
-            activities.forEach(item -> {
+        try {
+            DataLoader.loadList("ActivityConfig.json", ActivityConfigItem.class).forEach(item -> {
                 var activityData = GameData.getActivityDataMap().get(item.getActivityId());
                 if (activityData == null) {
                     Grasscutter.getLogger().warn("activity {} not exist.", item.getActivityId());
diff --git a/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
index 3a4b207f..033157aa 100644
--- a/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
+++ b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
@@ -13,6 +13,7 @@ import emu.grasscutter.game.player.Player;
 import emu.grasscutter.game.props.ActionReason;
 import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
 import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
+import emu.grasscutter.utils.Utils;
 import lombok.AccessLevel;
 import lombok.Builder;
 import lombok.Data;
@@ -68,7 +69,7 @@ public class PlayerActivityData {
     }
 
     public void setDetail(Object detail){
-        this.detail = Grasscutter.getGsonFactory().toJson(detail);
+        this.detail = Utils.jsonEncode(detail);
     }
 
     public void takeWatcherReward(int watcherId) {
diff --git a/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
index 7b8b8100..4c0930af 100644
--- a/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
+++ b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
@@ -8,6 +8,7 @@ import emu.grasscutter.game.props.ActivityType;
 import emu.grasscutter.net.proto.ActivityInfoOuterClass;
 import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
 import emu.grasscutter.net.proto.MusicGameActivityDetailInfoOuterClass;
+import emu.grasscutter.utils.Utils;
 
 import java.util.stream.Collectors;
 
@@ -47,8 +48,7 @@ public class MusicGameActivityHandler extends ActivityHandler {
             playerActivityData.save();
         }
 
-        return Grasscutter.getGsonFactory().fromJson(playerActivityData.getDetail(),
-            MusicGamePlayerData.class);
+        return Utils.jsonDecode(playerActivityData.getDetail(), MusicGamePlayerData.class);
     }
 
     public boolean setMusicGameRecord(PlayerActivityData playerActivityData, MusicGamePlayerData.MusicGameRecord newRecord) {
diff --git a/src/main/java/emu/grasscutter/game/combine/CombineManger.java b/src/main/java/emu/grasscutter/game/combine/CombineManger.java
index 4aaacb88..20b2e9a4 100644
--- a/src/main/java/emu/grasscutter/game/combine/CombineManger.java
+++ b/src/main/java/emu/grasscutter/game/combine/CombineManger.java
@@ -6,8 +6,6 @@ import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.common.ItemParamData;
 import emu.grasscutter.data.excels.CombineData;
 import emu.grasscutter.game.inventory.GameItem;
-import emu.grasscutter.game.inventory.Inventory;
-import emu.grasscutter.game.inventory.ItemType;
 import emu.grasscutter.game.player.Player;
 import emu.grasscutter.game.props.ActionReason;
 import emu.grasscutter.game.props.ItemUseOp;
@@ -19,18 +17,12 @@ import emu.grasscutter.server.packet.send.PacketCombineFormulaDataNotify;
 import emu.grasscutter.server.packet.send.PacketCombineRsp;
 import emu.grasscutter.server.packet.send.PacketReliquaryDecomposeRsp;
 import emu.grasscutter.utils.Utils;
-import it.unimi.dsi.fastutil.Pair;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 
-import com.google.gson.reflect.TypeToken;
-
 public class CombineManger extends BaseGameSystem {
     private final static Int2ObjectMap<List<Integer>> reliquaryDecomposeData = new Int2ObjectOpenHashMap<>();
 
@@ -40,13 +32,10 @@ public class CombineManger extends BaseGameSystem {
 
     public static void initialize() {
         // Read the data we need for strongbox.
-        try (Reader fileReader = DataLoader.loadReader("ReliquaryDecompose.json")) {
-            List<ReliquaryDecomposeEntry> decomposeEntries = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ReliquaryDecomposeEntry.class).getType());
-
-            for (ReliquaryDecomposeEntry entry : decomposeEntries) {
+        try {
+            DataLoader.loadList("ReliquaryDecompose.json", ReliquaryDecomposeEntry.class).forEach(entry -> {
                 reliquaryDecomposeData.put(entry.getConfigId(), entry.getItems());
-            }
-
+            });
             Grasscutter.getLogger().debug("Loaded {} reliquary decompose entries.", reliquaryDecomposeData.size());
         }
         catch (Exception ex) {
diff --git a/src/main/java/emu/grasscutter/game/drop/DropSystem.java b/src/main/java/emu/grasscutter/game/drop/DropSystem.java
index b2b28852..7c4dc44b 100644
--- a/src/main/java/emu/grasscutter/game/drop/DropSystem.java
+++ b/src/main/java/emu/grasscutter/game/drop/DropSystem.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.game.drop;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.data.GameData;
@@ -19,9 +18,6 @@ import emu.grasscutter.utils.Utils;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.Collection;
 import java.util.List;
 
 public class DropSystem extends BaseGameSystem {
@@ -38,9 +34,9 @@ public class DropSystem extends BaseGameSystem {
     }
 
     public synchronized void load() {
-        try (Reader fileReader = DataLoader.loadReader("Drop.json")) {
-            getDropData().clear();
-            List<DropInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType());
+        getDropData().clear();
+        try {
+            List<DropInfo> banners = DataLoader.loadList("Drop.json", DropInfo.class);
             if (banners.size() > 0) {
                 for (DropInfo di : banners) {
                     getDropData().put(di.getMonsterId(), di.getDropDataList());
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/DungeonChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/DungeonChallenge.java
index 844f23c5..3455b4ad 100644
--- a/src/main/java/emu/grasscutter/game/dungeons/challenge/DungeonChallenge.java
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/DungeonChallenge.java
@@ -24,16 +24,11 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
 
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
-import com.google.gson.reflect.TypeToken;
-
 public class DungeonChallenge extends WorldChallenge {
 
     /**
@@ -46,13 +41,10 @@ public class DungeonChallenge extends WorldChallenge {
 
     public static void initialize() {
         // Read the data we need for dungeon rewards drops.
-        try (Reader fileReader = DataLoader.loadReader("DungeonDrop.json")) {
-            List<DungeonDrop> dungeonDropList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DungeonDrop.class).getType());
-
-            for (DungeonDrop entry : dungeonDropList) {
+        try {
+            DataLoader.loadList("DungeonDrop.json", DungeonDrop.class).forEach(entry -> {
                 dungeonDropData.put(entry.getDungeonId(), entry.getDrops());
-            }
-
+            });
             Grasscutter.getLogger().debug("Loaded {} dungeon drop data entries.", dungeonDropData.size());
         }
         catch (Exception ex) {
diff --git a/src/main/java/emu/grasscutter/game/expedition/ExpeditionSystem.java b/src/main/java/emu/grasscutter/game/expedition/ExpeditionSystem.java
index ddc0535d..351b81b3 100644
--- a/src/main/java/emu/grasscutter/game/expedition/ExpeditionSystem.java
+++ b/src/main/java/emu/grasscutter/game/expedition/ExpeditionSystem.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.game.expedition;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.server.game.BaseGameSystem;
@@ -8,12 +7,6 @@ import emu.grasscutter.server.game.GameServer;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
-import static emu.grasscutter.config.Configuration.*;
-
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.Collection;
 import java.util.List;
 
 public class ExpeditionSystem extends BaseGameSystem {
@@ -30,9 +23,9 @@ public class ExpeditionSystem extends BaseGameSystem {
     }
 
     public synchronized void load() {
-        try (Reader fileReader = DataLoader.loadReader("ExpeditionReward.json")) {
-            getExpeditionRewardDataList().clear();
-            List<ExpeditionRewardInfo> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ExpeditionRewardInfo.class).getType());
+        getExpeditionRewardDataList().clear();
+        try {
+            List<ExpeditionRewardInfo> banners = DataLoader.loadList("ExpeditionReward.json", ExpeditionRewardInfo.class);
             if (banners.size() > 0) {
                 for (ExpeditionRewardInfo di : banners) {
                     getExpeditionRewardDataList().put(di.getExpId(), di.getExpeditionRewardDataList());
diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaSystem.java b/src/main/java/emu/grasscutter/game/gacha/GachaSystem.java
index 1d24c0b7..afe1bb21 100644
--- a/src/main/java/emu/grasscutter/game/gacha/GachaSystem.java
+++ b/src/main/java/emu/grasscutter/game/gacha/GachaSystem.java
@@ -3,18 +3,11 @@ package emu.grasscutter.game.gacha;
 import static emu.grasscutter.config.Configuration.*;
 
 import java.io.File;
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.nio.file.*;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 
-import com.google.gson.reflect.TypeToken;
-
 import com.sun.nio.file.SensitivityWatchEventModifier;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
@@ -76,9 +69,9 @@ public class GachaSystem extends BaseGameSystem {
     }
 
     public synchronized void load() {
-        try (Reader fileReader = DataLoader.loadReader("Banners.json")) {
-            getGachaBanners().clear();
-            List<GachaBanner> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, GachaBanner.class).getType());
+        getGachaBanners().clear();
+        try {
+            List<GachaBanner> banners = DataLoader.loadList("Banners.json", GachaBanner.class);
             if (banners.size() > 0) {
                 for (GachaBanner banner : banners) {
                     getGachaBanners().put(banner.getScheduleId(), banner);
diff --git a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java
index cf449977..3fc42d73 100644
--- a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java
+++ b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java
@@ -31,9 +31,6 @@ import emu.grasscutter.utils.Position;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -42,7 +39,6 @@ import java.util.concurrent.ThreadLocalRandom;
 
 import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
 
-import com.google.gson.reflect.TypeToken;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 public class EnergyManager extends BasePlayerManager {
@@ -60,12 +56,10 @@ public class EnergyManager extends BasePlayerManager {
 
     public static void initialize() {
         // Read the data we need for monster energy drops.
-        try (Reader fileReader = DataLoader.loadReader("EnergyDrop.json")) {
-            List<EnergyDropEntry> energyDropList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, EnergyDropEntry.class).getType());
-
-            for (EnergyDropEntry entry : energyDropList) {
+        try {
+            DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class).forEach(entry -> {
                 energyDropData.put(entry.getDropId(), entry.getDropList());
-            }
+            });
 
             Grasscutter.getLogger().debug("Energy drop data successfully loaded.");
         }
@@ -74,12 +68,10 @@ public class EnergyManager extends BasePlayerManager {
         }
 
         // Read the data for particle generation from skills
-        try (Reader fileReader = new InputStreamReader(DataLoader.load("SkillParticleGeneration.json"))) {
-            List<SkillParticleGenerationEntry> skillParticleGenerationList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, SkillParticleGenerationEntry.class).getType());
-
-            for (SkillParticleGenerationEntry entry : skillParticleGenerationList) {
+        try {
+            DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class).forEach(entry -> {
                 skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList());
-            }
+            });
 
             Grasscutter.getLogger().debug("Skill particle generation data successfully loaded.");
         }
diff --git a/src/main/java/emu/grasscutter/game/shop/ShopSystem.java b/src/main/java/emu/grasscutter/game/shop/ShopSystem.java
index 006af71d..ef2a39d1 100644
--- a/src/main/java/emu/grasscutter/game/shop/ShopSystem.java
+++ b/src/main/java/emu/grasscutter/game/shop/ShopSystem.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.game.shop;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.data.GameData;
@@ -14,11 +13,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
 import static emu.grasscutter.config.Configuration.*;
 
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 
@@ -60,9 +55,9 @@ public class ShopSystem extends BaseGameSystem {
     }
 
     private void loadShop() {
-        try (Reader fileReader = DataLoader.loadReader("Shop.json")) {
-            getShopData().clear();
-            List<ShopTable> banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopTable.class).getType());
+        getShopData().clear();
+        try {
+            List<ShopTable> banners = DataLoader.loadList("Shop.json", ShopTable.class);
             if (banners.size() > 0) {
                 for (ShopTable shopTable : banners) {
                     for (ShopInfo cost : shopTable.getItems()) {
@@ -104,9 +99,9 @@ public class ShopSystem extends BaseGameSystem {
     }
 
     private void loadShopChest() {
-        try (Reader fileReader = DataLoader.loadReader("ShopChest.json")) {
-            getShopChestData().clear();
-            List<ShopChestTable> shopChestTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestTable.class).getType());
+        getShopChestData().clear();
+        try {
+            List<ShopChestTable> shopChestTableList = DataLoader.loadList("ShopChest.json", ShopChestTable.class);
             if (shopChestTableList.size() > 0) {
                 getShopChestData().addAll(shopChestTableList);
                 Grasscutter.getLogger().debug("ShopChest data successfully loaded.");
@@ -119,9 +114,9 @@ public class ShopSystem extends BaseGameSystem {
     }
 
     private void loadShopChestBatchUse() {
-        try (Reader fileReader = DataLoader.loadReader("ShopChestBatchUse.json")) {
-            getShopChestBatchUseData().clear();
-            List<ShopChestBatchUseTable> shopChestBatchUseTableList = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopChestBatchUseTable.class).getType());
+        getShopChestBatchUseData().clear();
+        try {
+            List<ShopChestBatchUseTable> shopChestBatchUseTableList = DataLoader.loadList("ShopChestBatchUse.json", ShopChestBatchUseTable.class);
             if (shopChestBatchUseTableList.size() > 0) {
                 getShopChestBatchUseData().addAll(shopChestBatchUseTableList);
                 Grasscutter.getLogger().debug("ShopChestBatchUse data successfully loaded.");
diff --git a/src/main/java/emu/grasscutter/game/systems/AnnouncementSystem.java b/src/main/java/emu/grasscutter/game/systems/AnnouncementSystem.java
index 510a3f61..e2741523 100644
--- a/src/main/java/emu/grasscutter/game/systems/AnnouncementSystem.java
+++ b/src/main/java/emu/grasscutter/game/systems/AnnouncementSystem.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.game.systems;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.game.player.Player;
@@ -16,7 +15,6 @@ import lombok.Data;
 import lombok.Getter;
 import lombok.experimental.FieldDefaults;
 
-import java.io.InputStreamReader;
 import java.util.*;
 
 @Getter
@@ -30,9 +28,8 @@ public class AnnouncementSystem extends BaseGameSystem {
     }
 
     private int loadConfig() {
-        try (var fileReader = DataLoader.loadReader("Announcement.json")) {
-            List<AnnounceConfigItem> announceConfigItems = Grasscutter.getGsonFactory().fromJson(fileReader,
-                TypeToken.getParameterized(List.class, AnnounceConfigItem.class).getType());
+        try {
+            List<AnnounceConfigItem> announceConfigItems = DataLoader.loadList("Announcement.json", AnnounceConfigItem.class);
 
             announceConfigItemMap.clear();
             announceConfigItems.forEach(i -> announceConfigItemMap.put(i.getTemplateId(), i));
diff --git a/src/main/java/emu/grasscutter/game/tower/TowerSystem.java b/src/main/java/emu/grasscutter/game/tower/TowerSystem.java
index 1282d710..014c5ddd 100644
--- a/src/main/java/emu/grasscutter/game/tower/TowerSystem.java
+++ b/src/main/java/emu/grasscutter/game/tower/TowerSystem.java
@@ -7,11 +7,6 @@ import emu.grasscutter.data.excels.TowerScheduleData;
 import emu.grasscutter.server.game.BaseGameSystem;
 import emu.grasscutter.server.game.GameServer;
 
-import static emu.grasscutter.config.Configuration.*;
-
-import java.io.FileReader;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -25,8 +20,8 @@ public class TowerSystem extends BaseGameSystem {
     private TowerScheduleConfig towerScheduleConfig;
 
     public synchronized void load() {
-        try (Reader fileReader = DataLoader.loadReader("TowerSchedule.json")) {
-            towerScheduleConfig = Grasscutter.getGsonFactory().fromJson(fileReader, TowerScheduleConfig.class);
+        try {
+            towerScheduleConfig = DataLoader.loadClass("TowerSchedule.json", TowerScheduleConfig.class);
         } catch (Exception e) {
             Grasscutter.getLogger().error("Unable to load tower schedule config.", e);
         }
diff --git a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java
index 9251057a..4715434b 100644
--- a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java
+++ b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java
@@ -1,6 +1,5 @@
 package emu.grasscutter.game.world;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.DataLoader;
 import emu.grasscutter.data.GameData;
@@ -17,9 +16,6 @@ import emu.grasscutter.scripts.data.SceneMonster;
 import emu.grasscutter.server.game.BaseGameSystem;
 import emu.grasscutter.server.game.GameServer;
 
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -42,15 +38,11 @@ public class WorldDataSystem extends BaseGameSystem {
         // set the special chest first
         chestInteractHandlerMap.put("SceneObj_Chest_Flora", new BossChestInteractHandler());
 
-        try (Reader reader = DataLoader.loadReader("ChestReward.json")) {
-            List<ChestReward> chestReward = Grasscutter.getGsonFactory().fromJson(
-                    reader,
-                    TypeToken.getParameterized(List.class, ChestReward.class).getType());
-
-            chestReward.forEach(reward ->
-                    reward.getObjNames().forEach(
-                            name -> chestInteractHandlerMap.putIfAbsent(name, new NormalChestInteractHandler(reward))));
-
+        try {
+            DataLoader.loadList("ChestReward.json", ChestReward.class)
+                .forEach(reward ->
+                    reward.getObjNames().forEach(name ->
+                        chestInteractHandlerMap.putIfAbsent(name, new NormalChestInteractHandler(reward))));
         } catch (Exception e) {
             Grasscutter.getLogger().error("Unable to load chest reward config.", e);
         }
diff --git a/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtils.java b/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtils.java
index ac915b2a..009aa940 100644
--- a/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtils.java
+++ b/src/main/java/emu/grasscutter/net/packet/PacketOpcodesUtils.java
@@ -12,6 +12,7 @@ import java.util.stream.Collectors;
 
 import emu.grasscutter.GameConstants;
 import emu.grasscutter.Grasscutter;
+import emu.grasscutter.utils.Utils;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 
@@ -59,7 +60,7 @@ public class PacketOpcodesUtils {
                     .filter(e -> e.getIntKey() > 0)
                     .collect(Collectors.toMap(Int2ObjectMap.Entry::getIntKey, Int2ObjectMap.Entry::getValue, (k, v) -> v, TreeMap::new));
             // Write to file
-            writer.write(Grasscutter.getGsonFactory().toJson(packetIds));
+            writer.write(Utils.jsonEncode(packetIds));
             Grasscutter.getLogger().info("Dumped packet ids.");
         } catch (IOException e) {
             e.printStackTrace();
diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java
index d2df184d..72edc9df 100644
--- a/src/main/java/emu/grasscutter/plugin/PluginManager.java
+++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java
@@ -82,7 +82,7 @@ public final class PluginManager {
                     InputStreamReader fileReader = new InputStreamReader(configFile.openStream());
 
                     // Create a plugin config instance from the config file.
-                    PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
+                    PluginConfig pluginConfig = Utils.loadJsonToClass(fileReader, PluginConfig.class);
                     // Check if the plugin config is valid.
                     if (!pluginConfig.validate()) {
                         Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file.");
diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java
index 27ae4ee5..6249739d 100644
--- a/src/main/java/emu/grasscutter/utils/Language.java
+++ b/src/main/java/emu/grasscutter/utils/Language.java
@@ -130,7 +130,7 @@ public final class Language {
         languageCode = description.getLanguageCode();
 
         try {
-            languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class);
+            languageData = Utils.jsonDecode(Utils.readFromInputStream(description.getLanguageFile()), JsonObject.class);
         } catch (Exception exception) {
             Grasscutter.getLogger().warn("Failed to load language file: " + description.getLanguageCode(), exception);
         }
diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java
index 45f6b563..98d66e7b 100644
--- a/src/main/java/emu/grasscutter/utils/Utils.java
+++ b/src/main/java/emu/grasscutter/utils/Utils.java
@@ -20,12 +20,25 @@ import it.unimi.dsi.fastutil.ints.IntList;
 
 import org.slf4j.Logger;
 
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
 import javax.annotation.Nullable;
 
 import static emu.grasscutter.utils.Language.translate;
 
 @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
 public final class Utils {
+    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+    @Deprecated(forRemoval = true)
+    public static Gson getGsonFactory() {
+        return gson;
+    }
+
     public static final Random random = new Random();
 
     public static int randomRange(int min, int max) {
@@ -158,8 +171,7 @@ public final class Utils {
      * @param object The object to log.
      */
     public static void logObject(Object object) {
-        String asJson = Grasscutter.getGsonFactory().toJson(object);
-        Grasscutter.getLogger().info(asJson);
+        Grasscutter.getLogger().info(jsonEncode(object));
     }
 
     /**
@@ -357,6 +369,44 @@ public final class Utils {
         return Base64.getDecoder().decode(toDecode);
     }
 
+    /*
+     * Encode an object to a JSON string
+     */
+    public static String jsonEncode(Object object) {
+        return gson.toJson(object);
+    }
+
+    public static <T> T jsonDecode(JsonElement jsonElement, Class<T> classType) throws JsonSyntaxException {
+        return gson.fromJson(jsonElement, classType);
+    }
+
+    public static <T> T loadJsonToClass(InputStreamReader fileReader, Class<T> classType) throws IOException {
+        return gson.fromJson(fileReader, classType);
+    }
+    public static <T> T loadJsonToClass(String filename, Class<T> classType) throws IOException {
+        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
+            return loadJsonToClass(fileReader, classType);
+        }
+    }
+
+    public static <T> List<T> loadJsonToList(InputStreamReader fileReader, Class<T> classType) throws IOException {
+        return gson.fromJson(fileReader, TypeToken.getParameterized(List.class, classType).getType());
+    }
+    public static <T> List<T> loadJsonToList(String filename, Class<T> classType) throws IOException {
+        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
+            return loadJsonToList(fileReader, classType);
+        }
+    }
+
+    public static <T1,T2> Map<T1,T2> loadJsonToMap(InputStreamReader fileReader, Class<T1> keyType, Class<T2> valueType) throws IOException {
+        return gson.fromJson(fileReader, TypeToken.getParameterized(Map.class, keyType, valueType).getType());
+    }
+    public static <T1,T2> Map<T1,T2> loadJsonToMap(String filename, Class<T1> keyType, Class<T2> valueType) throws IOException {
+        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
+            return loadJsonToMap(fileReader, keyType, valueType);
+        }
+    }
+
     /**
      * Safely JSON decodes a given string.
      * @param jsonData The JSON-encoded data.
@@ -364,7 +414,7 @@ public final class Utils {
      */
     public static <T> T jsonDecode(String jsonData, Class<T> classType) {
         try {
-            return Grasscutter.getGsonFactory().fromJson(jsonData, classType);
+            return gson.fromJson(jsonData, classType);
         } catch (Exception ignored) {
             return null;
         }
-- 
GitLab