From fbc0219cba8674a981682fc8f5f13508b84e7ede Mon Sep 17 00:00:00 2001
From: AnimeGitB <AnimeGitB@bigblueball.in>
Date: Fri, 23 Sep 2022 18:10:46 +0930
Subject: [PATCH] Allow loading Resources from zip files Move Resources loading
 from String filenames to Paths Add zip support

---
 .../java/emu/grasscutter/Grasscutter.java     |   3 +-
 .../grasscutter/config/ConfigContainer.java   |   2 +-
 .../emu/grasscutter/config/Configuration.java |  58 ++-
 .../emu/grasscutter/data/ResourceLoader.java  | 445 ++++++++----------
 .../java/emu/grasscutter/tools/Tools.java     |  14 +-
 .../java/emu/grasscutter/utils/FileUtils.java |   5 +-
 .../java/emu/grasscutter/utils/JsonUtils.java |  30 +-
 .../java/emu/grasscutter/utils/Language.java  |   4 +-
 .../java/emu/grasscutter/utils/Utils.java     |  10 +-
 9 files changed, 295 insertions(+), 276 deletions(-)

diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java
index a8bd09c2..1f38fa5c 100644
--- a/src/main/java/emu/grasscutter/Grasscutter.java
+++ b/src/main/java/emu/grasscutter/Grasscutter.java
@@ -45,7 +45,6 @@ import javax.annotation.Nullable;
 import java.io.*;
 import java.util.Calendar;
 
-import static emu.grasscutter.config.Configuration.DATA;
 import static emu.grasscutter.config.Configuration.SERVER;
 import static emu.grasscutter.utils.Language.translate;
 
@@ -203,7 +202,7 @@ public final class Grasscutter {
 
         // If the file already exists, we attempt to load it.
         try {
-            config = JsonUtils.loadToClass(configFile.getPath(), ConfigContainer.class);
+            config = JsonUtils.loadToClass(configFile.toPath(), 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);
diff --git a/src/main/java/emu/grasscutter/config/ConfigContainer.java b/src/main/java/emu/grasscutter/config/ConfigContainer.java
index 9417de7c..f8af1dae 100644
--- a/src/main/java/emu/grasscutter/config/ConfigContainer.java
+++ b/src/main/java/emu/grasscutter/config/ConfigContainer.java
@@ -26,7 +26,7 @@ public class ConfigContainer {
      */
     public static void updateConfig() {
         try { // Check if the server is using a legacy config.
-            JsonObject configObject = JsonUtils.loadToClass(Grasscutter.configFile.getPath(), JsonObject.class);
+            JsonObject configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class);
             if (!configObject.has("version")) {
                 Grasscutter.getLogger().info("Updating legacy ..");
                 Grasscutter.saveConfig(null);
diff --git a/src/main/java/emu/grasscutter/config/Configuration.java b/src/main/java/emu/grasscutter/config/Configuration.java
index 7c769b3a..bf008a41 100644
--- a/src/main/java/emu/grasscutter/config/Configuration.java
+++ b/src/main/java/emu/grasscutter/config/Configuration.java
@@ -1,7 +1,15 @@
 package emu.grasscutter.config;
 
 import java.util.Locale;
+import java.util.stream.Stream;
 
+import emu.grasscutter.Grasscutter;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import static emu.grasscutter.Grasscutter.config;
@@ -30,6 +38,42 @@ public final class Configuration extends ConfigContainer {
     private static final String PLUGINS_FOLDER = config.folderStructure.plugins;
     private static final String SCRIPTS_FOLDER = config.folderStructure.scripts;
     private static final String PACKETS_FOLDER = config.folderStructure.packets;
+    private static final FileSystem RESOURCES_FILE_SYSTEM;  // Not sure about lifetime rules on this one, might be safe to remove
+    private static final Path RESOURCES_PATH;
+    static {
+        FileSystem fs = null;
+        Path path = Path.of(RESOURCES_FOLDER);
+        if (RESOURCES_FOLDER.endsWith(".zip")) {  // Would be nice to support .tar.gz too at some point, but it doesn't come for free in Java
+            try {
+                fs = FileSystems.newFileSystem(path);
+            } catch (IOException e) {
+                Grasscutter.getLogger().error("Failed to load resources zip \"" + RESOURCES_FOLDER + "\"");
+            }
+        }
+
+        if (fs != null) {
+            var root = fs.getPath("");
+            try (Stream<Path> pathStream = java.nio.file.Files.find(root, 3, (p, a) -> {
+                        var filename = p.getFileName();
+                        if (filename == null) return false;
+                        return filename.toString().equals("ExcelBinOutput");
+            })) {
+                var excelBinOutput = pathStream.findFirst();
+                if (excelBinOutput.isPresent()) {
+                    path = excelBinOutput.get().getParent();
+                    if (path == null)
+                        path = root;
+                    Grasscutter.getLogger().debug("Resources will be loaded from \"" + RESOURCES_FOLDER + "/" + path.toString() + "\"");
+                } else {
+                    Grasscutter.getLogger().error("Failed to find ExcelBinOutput in resources zip \"" + RESOURCES_FOLDER + "\"");
+                }
+            } catch (IOException e) {
+                Grasscutter.getLogger().error("Failed to scan resources zip \"" + RESOURCES_FOLDER + "\"");
+            }
+        }
+        RESOURCES_FILE_SYSTEM = fs;
+        RESOURCES_PATH = path;
+    };
 
     public static final Server SERVER = config.server;
     public static final Database DATABASE = config.databaseInfo;
@@ -54,11 +98,15 @@ public final class Configuration extends ConfigContainer {
     }
 
     public static String DATA(String path) {
-        return Paths.get(DATA_FOLDER, path).toString();
+        return Path.of(DATA_FOLDER, path).toString();
+    }
+
+    public static Path getResourcePath(String path) {
+        return RESOURCES_PATH.resolve(path);
     }
 
     public static String RESOURCE(String path) {
-        return Paths.get(RESOURCES_FOLDER, path).toString();
+        return getResourcePath(path).toString();
     }
 
     public static String PLUGIN() {
@@ -66,15 +114,15 @@ public final class Configuration extends ConfigContainer {
     }
 
     public static String PLUGIN(String path) {
-        return Paths.get(PLUGINS_FOLDER, path).toString();
+        return Path.of(PLUGINS_FOLDER, path).toString();
     }
 
     public static String SCRIPT(String path) {
-        return Paths.get(SCRIPTS_FOLDER, path).toString();
+        return Path.of(SCRIPTS_FOLDER, path).toString();
     }
 
     public static String PACKET(String path) {
-        return Paths.get(PACKETS_FOLDER, path).toString();
+        return Path.of(PACKETS_FOLDER, path).toString();
     }
 
     /**
diff --git a/src/main/java/emu/grasscutter/data/ResourceLoader.java b/src/main/java/emu/grasscutter/data/ResourceLoader.java
index 0a580d46..d7155816 100644
--- a/src/main/java/emu/grasscutter/data/ResourceLoader.java
+++ b/src/main/java/emu/grasscutter/data/ResourceLoader.java
@@ -5,7 +5,6 @@ import com.google.gson.annotations.SerializedName;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.binout.*;
 import emu.grasscutter.data.binout.AbilityModifier.AbilityConfigData;
-import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
 import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierActionType;
 import emu.grasscutter.data.common.PointData;
 import emu.grasscutter.data.common.ScenePointConfig;
@@ -16,21 +15,19 @@ import emu.grasscutter.game.world.SpawnDataEntry.GridBlockId;
 import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry;
 import emu.grasscutter.scripts.SceneIndexManager;
 import emu.grasscutter.utils.JsonUtils;
-import emu.grasscutter.utils.Utils;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
-import lombok.SneakyThrows;
+import lombok.val;
+
 import org.reflections.Reflections;
 
 import java.io.*;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.*;
-import java.util.Map.Entry;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 import static emu.grasscutter.config.Configuration.DATA;
-import static emu.grasscutter.config.Configuration.RESOURCE;
+import static emu.grasscutter.config.Configuration.getResourcePath;
 import static emu.grasscutter.utils.Language.translate;
 
 public class ResourceLoader {
@@ -105,7 +102,7 @@ public class ResourceLoader {
             try {
                 loadFromResource(resourceDefinition, type, map, doReload);
             } catch (Exception e) {
-                Grasscutter.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e);
+                Grasscutter.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e.getLocalizedMessage());
             }
         }
     }
@@ -123,7 +120,7 @@ public class ResourceLoader {
 
     @SuppressWarnings({"rawtypes", "unchecked"})
     protected static <T> void loadFromResource(Class<T> c, String fileName, Int2ObjectMap map) throws Exception {
-        List<T> list = JsonUtils.loadToList(RESOURCE("ExcelBinOutput/" + fileName), c);
+        List<T> list = JsonUtils.loadToList(getResourcePath("ExcelBinOutput/" + fileName), c);
 
         for (T o : list) {
             GameResource res = (GameResource) o;
@@ -133,50 +130,42 @@ public class ResourceLoader {
     }
 
     private static void loadScenePoints() {
-        Pattern pattern = Pattern.compile("(?<=scene)(.*?)(?=_point.json)");
-        File folder = new File(RESOURCE("BinOutput/Scene/Point"));
-
-        if (!folder.isDirectory() || !folder.exists() || folder.listFiles() == null) {
-            Grasscutter.getLogger().error("Scene point files cannot be found, you cannot use teleport waypoints!");
-            return;
-        }
+        val pattern = Pattern.compile("scene([0-9]+)_point\\.json");
+        try {
+            Files.newDirectoryStream(getResourcePath("BinOutput/Scene/Point"), "scene*_point.json").forEach(path -> {
+                val matcher = pattern.matcher(path.getFileName().toString());
+                if (!matcher.find()) return;
+                int sceneId = Integer.parseInt(matcher.group(1));
+                ScenePointConfig config;
 
-        for (File file : Objects.requireNonNull(folder.listFiles())) {
-            ScenePointConfig config;
-            Integer sceneId;
+                try {
+                    config = JsonUtils.loadToClass(path, ScenePointConfig.class);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    return;
+                }
 
-            Matcher matcher = pattern.matcher(file.getName());
-            if (matcher.find()) {
-                sceneId = Integer.parseInt(matcher.group(1));
-            } else {
-                continue;
-            }
+                if (config.points == null) return;
 
-            try {
-                config = JsonUtils.loadToClass(file.getPath(), ScenePointConfig.class);
-            } catch (Exception e) {
-                e.printStackTrace();
-                continue;
-            }
+                List<Integer> scenePoints = new ArrayList<>();
+                for (Map.Entry<String, JsonElement> entry : config.points.entrySet()) {
+                    String key = entry.getKey();
+                    String name = sceneId + "_" + key;
+                    int id = Integer.parseInt(key);
+                    PointData pointData = JsonUtils.decode(entry.getValue(), PointData.class);
+                    pointData.setId(id);
 
-            if (config.points == null) {
-                continue;
-            }
+                    GameData.getScenePointIdList().add(id);
+                    GameData.getScenePointEntries().put(name, new ScenePointEntry(name, pointData));
+                    scenePoints.add(id);
 
-            List<Integer> scenePoints = new ArrayList<>();
-            for (Map.Entry<String, JsonElement> entry : config.points.entrySet()) {
-                int id = Integer.parseInt(entry.getKey());
-                String name = sceneId + "_" + entry.getKey();
-                PointData pointData = JsonUtils.decode(entry.getValue(), PointData.class);
-                pointData.setId(id);
-
-                GameData.getScenePointIdList().add(id);
-                GameData.getScenePointEntries().put(name, new ScenePointEntry(name, pointData));
-                scenePoints.add(id);
-
-                pointData.updateDailyDungeon();
-            }
-            GameData.getScenePointsPerScene().put(sceneId, scenePoints);
+                    pointData.updateDailyDungeon();
+                }
+                GameData.getScenePointsPerScene().put(sceneId, scenePoints);
+            });
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Scene point files cannot be found, you cannot use teleport waypoints!");
+            return;
         }
     }
 
@@ -190,47 +179,40 @@ public class ResourceLoader {
 
         if (embryoList == null) {
             // Load from BinOutput
-            Pattern pattern = Pattern.compile("(?<=ConfigAvatar_)(.*?)(?=.json)");
-
-            embryoList = new ArrayList<>();
-            File folder = new File(Utils.toFilePath(RESOURCE("BinOutput/Avatar/")));
-            File[] files = folder.listFiles();
-            if (files == null) {
-                Grasscutter.getLogger().error("Error loading ability embryos: no files found in " + folder.getAbsolutePath());
-                return;
-            }
+            val pattern = Pattern.compile("ConfigAvatar_(.+?)\\.json");
 
-            for (File file : files) {
-                AvatarConfig config;
-                String avatarName;
-
-                Matcher matcher = pattern.matcher(file.getName());
-                if (matcher.find()) {
-                    avatarName = matcher.group(0);
-                } else {
-                    continue;
-                }
+            val l = new ArrayList<AbilityEmbryoEntry>();
+            try {
+                Files.newDirectoryStream(getResourcePath("BinOutput/Avatar/"), "ConfigAvatar_*.json").forEach(path -> {
+                    val matcher = pattern.matcher(path.getFileName().toString());
+                    if (!matcher.find()) return;
+                    String avatarName = matcher.group(1);
+                    AvatarConfig config;
 
-                try {
-                    config = JsonUtils.loadToClass(file.getPath(), AvatarConfig.class);
-                } catch (Exception e) {
-                    e.printStackTrace();
-                    continue;
-                }
+                    try {
+                        config = JsonUtils.loadToClass(path, AvatarConfig.class);
+                    } catch (Exception e) {
+                        Grasscutter.getLogger().error("Error loading player ability embryos:", e);
+                        return;
+                    }
 
-                if (config.abilities == null) {
-                    continue;
-                }
+                    if (config.abilities == null) return;
 
-                int s = config.abilities.size();
-                AbilityEmbryoEntry al = new AbilityEmbryoEntry(avatarName, config.abilities.stream().map(Object::toString).toArray(size -> new String[s]));
-                embryoList.add(al);
+                    int s = config.abilities.size();
+                    AbilityEmbryoEntry al = new AbilityEmbryoEntry(avatarName, config.abilities.stream().map(Object::toString).toArray(size -> new String[s]));
+                    l.add(al);
+                });
+            } catch (IOException e) {
+                Grasscutter.getLogger().error("Error loading ability embryos: no files found");
+                return;
             }
 
+            embryoList = l;
+
             try {
-                GameDepot.setPlayerAbilities(JsonUtils.loadToMap(RESOURCE("BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), String.class, AvatarConfig.class));
-            } catch (Exception e) {
-                e.printStackTrace();
+                GameDepot.setPlayerAbilities(JsonUtils.loadToMap(getResourcePath("BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), String.class, AvatarConfig.class));
+            } catch (IOException e) {
+                Grasscutter.getLogger().error("Error loading player abilities:", e);
             }
         }
 
@@ -246,64 +228,54 @@ public class ResourceLoader {
 
     private static void loadAbilityModifiers() {
         // Load from BinOutput
-        File folder = new File(Utils.toFilePath(RESOURCE("BinOutput/Ability/Temp/AvatarAbilities/")));
-        File[] files = folder.listFiles();
-        if (files == null) {
-            Grasscutter.getLogger().error("Error loading ability modifiers: no files found in " + folder.getAbsolutePath());
-            return;
-        }
-
-        for (File file : files) {
-            List<AbilityConfigData> abilityConfigList;
-
-            try {
-                abilityConfigList = JsonUtils.loadToList(file.getPath(), AbilityConfigData.class);
-            } catch (Exception e) {
-                e.printStackTrace();
-                continue;
-            }
+        try {
+            Files.newDirectoryStream(getResourcePath("BinOutput/Ability/Temp/AvatarAbilities/")).forEach(path -> {
+                List<AbilityConfigData> abilityConfigList;
 
-            for (AbilityConfigData data : abilityConfigList) {
-                if (data.Default.modifiers == null || data.Default.modifiers.size() == 0) {
-                    continue;
+                try {
+                    abilityConfigList = JsonUtils.loadToList(path, AbilityConfigData.class);
+                } catch (IOException e) {
+                    Grasscutter.getLogger().error("Error loading ability modifiers from path " + path.toString() + ": ", e);
+                    return;
                 }
 
-                AbilityModifierEntry modifierEntry = new AbilityModifierEntry(data.Default.abilityName);
-
-                for (Entry<String, AbilityModifier> entry : data.Default.modifiers.entrySet()) {
-                    AbilityModifier modifier = entry.getValue();
+                abilityConfigList.forEach(data -> {
+                    if (data.Default.modifiers == null || data.Default.modifiers.size() == 0) {
+                        return;
+                    }
 
-                    // Stare.
-                    if (modifier.onAdded != null) {
-                        for (AbilityModifierAction action : modifier.onAdded) {
-                            if (action.$type.contains("HealHP")) {
+                    String name = data.Default.abilityName;
+                    AbilityModifierEntry modifierEntry = new AbilityModifierEntry(name);
+                    data.Default.modifiers.forEach((key, modifier) -> {
+                        Stream.ofNullable(modifier.onAdded)
+                            .flatMap(Stream::of)
+                            .filter(action -> action.$type.contains("HealHP"))
+                            .forEach(action -> {
                                 action.type = AbilityModifierActionType.HealHP;
                                 modifierEntry.getOnAdded().add(action);
-                            }
-                        }
-                    }
-
-                    if (modifier.onThinkInterval != null) {
-                        for (AbilityModifierAction action : modifier.onThinkInterval) {
-                            if (action.$type.contains("HealHP")) {
+                            });
+                        Stream.ofNullable(modifier.onThinkInterval)
+                            .flatMap(Stream::of)
+                            .filter(action -> action.$type.contains("HealHP"))
+                            .forEach(action -> {
                                 action.type = AbilityModifierActionType.HealHP;
                                 modifierEntry.getOnThinkInterval().add(action);
-                            }
-                        }
-                    }
-
-                    if (modifier.onRemoved != null) {
-                        for (AbilityModifierAction action : modifier.onRemoved) {
-                            if (action.$type.contains("HealHP")) {
+                            });
+                        Stream.ofNullable(modifier.onRemoved)
+                            .flatMap(Stream::of)
+                            .filter(action -> action.$type.contains("HealHP"))
+                            .forEach(action -> {
                                 action.type = AbilityModifierActionType.HealHP;
                                 modifierEntry.getOnRemoved().add(action);
-                            }
-                        }
-                    }
-                }
-
-                GameData.getAbilityModifiers().put(modifierEntry.getName(), modifierEntry);
-            }
+                            });
+                    });
+
+                    GameData.getAbilityModifiers().put(name, modifierEntry);
+                });
+            });
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Error loading ability modifiers: ", e);
+            return;
         }
     }
 
@@ -353,30 +325,20 @@ public class ResourceLoader {
             Map<String, OpenConfigEntry> map = new TreeMap<>();
             String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"};
 
-            for (String name : folderNames) {
-                File folder = new File(Utils.toFilePath(RESOURCE(name)));
-                File[] files = folder.listFiles();
-                if (files == null) {
-                    Grasscutter.getLogger().error("Error loading open config: no files found in " + folder.getAbsolutePath()); return;
-                }
-
-                for (File file : files) {
-                    if (!file.getName().endsWith(".json")) {
-                        continue;
-                    }
-                    Map<String, OpenConfigData[]> config;
-
-                    try {
-                        config = JsonUtils.loadToMap(file.getPath(), String.class, OpenConfigData[].class);
-                    } catch (Exception e) {
-                        e.printStackTrace();
-                        continue;
-                    }
-
-                    for (Entry<String, OpenConfigData[]> e : config.entrySet()) {
-                        OpenConfigEntry entry = new OpenConfigEntry(e.getKey(), e.getValue());
-                        map.put(entry.getName(), entry);
-                    }
+            for (String folderName : folderNames) {
+                try {
+                    Files.newDirectoryStream(getResourcePath(folderName), "*.json").forEach(path -> {
+                        try {
+                            JsonUtils.loadToMap(path, String.class, OpenConfigData[].class)
+                                .forEach((name, data) -> map.put(name, new OpenConfigEntry(name, data)));
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                            return;
+                        }
+                    });
+                } catch (IOException e) {
+                    Grasscutter.getLogger().error("Error loading open config: no files found in " + folderName);
+                    return;
                 }
             }
 
@@ -394,37 +356,29 @@ public class ResourceLoader {
     }
 
     private static void loadQuests() {
-        File folder = new File(RESOURCE("BinOutput/Quest/"));
+        try {
+            Files.list(getResourcePath("BinOutput/Quest/")).forEach(path -> {
+                try {
+                    val mainQuest = JsonUtils.loadToClass(path, MainQuestData.class);
+                    GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest);
+                } catch (IOException e) {
 
-        if (!folder.exists()) {
+                }
+            });
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Quest data missing");
             return;
         }
 
-        for (File file : folder.listFiles()) {
-            MainQuestData mainQuest = null;
-
-            try {
-                mainQuest = JsonUtils.loadToClass(file.getPath(), MainQuestData.class);
-            } catch (Exception e) {
-                e.printStackTrace();
-                continue;
-            }
-
-            GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest);
-        }
-
         try {
-            List<QuestEncryptionKey> keys;
-            Int2ObjectMap<QuestEncryptionKey> questEncryptionMap = GameData.getMainQuestEncryptionMap();
+            val questEncryptionMap = GameData.getMainQuestEncryptionMap();
             String path = "QuestEncryptionKeys.json";
-            if (Utils.fileExists(RESOURCE(path))) {
-                keys = JsonUtils.loadToList(RESOURCE(path), QuestEncryptionKey.class);
-                keys.forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
-            }
-            if (Utils.fileExists(DATA(path))) {
-                keys = DataLoader.loadList(path, QuestEncryptionKey.class);
-                keys.forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
-            }
+            try {
+                JsonUtils.loadToList(getResourcePath(path), QuestEncryptionKey.class).forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
+            } catch (IOException | NullPointerException ignored) {}
+            try {
+                DataLoader.loadList(path, QuestEncryptionKey.class).forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
+            } catch (IOException | NullPointerException ignored) {}
             Grasscutter.getLogger().debug("Loaded {} quest keys.", questEncryptionMap.size());
         } catch (Exception e) {
             Grasscutter.getLogger().error("Unable to load quest keys.", e);
@@ -434,95 +388,84 @@ public class ResourceLoader {
     }
 
     public static void loadScriptSceneData() {
-        File folder = new File(RESOURCE("ScriptSceneData/"));
-
-        if (!folder.exists()) {
+        try {
+            Files.list(getResourcePath("ScriptSceneData/")).forEach(path -> {
+                try {
+                    GameData.getScriptSceneDataMap().put(path.getFileName().toString(), JsonUtils.loadToClass(path, ScriptSceneData.class));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    return;
+                }
+            });
+            Grasscutter.getLogger().debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas.");
+        } catch (IOException e) {
+            Grasscutter.getLogger().debug("ScriptSceneData folder missing or empty.");
             return;
         }
-
-        for (File file : folder.listFiles()) {
-            ScriptSceneData sceneData;
-            try {
-                sceneData = JsonUtils.loadToClass(file.getPath(), ScriptSceneData.class);
-            } catch (Exception e) {
-                e.printStackTrace();
-                continue;
-            }
-
-            GameData.getScriptSceneDataMap().put(file.getName(), sceneData);
-        }
-
-        Grasscutter.getLogger().debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas.");
     }
 
-    @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()) {
-                return;
-            }
-            try {
-                var sceneId = Integer.parseInt(matcher.group(1));
-                var data = JsonUtils.loadToClass(file.toString(), HomeworldDefaultSaveData.class);
-                GameData.getHomeworldDefaultSaveData().put(sceneId, data);
-            } catch (Exception ignored) {}
-        });
+        val pattern = Pattern.compile("scene([0-9]+)_home_config\\.json");
+        try {
+            Files.newDirectoryStream(getResourcePath("BinOutput/HomeworldDefaultSave"), "scene*_home_config.json").forEach(path -> {
+                val matcher = pattern.matcher(path.getFileName().toString());
+                if (!matcher.find()) return;
 
-        Grasscutter.getLogger().debug("Loaded " + GameData.getHomeworldDefaultSaveData().size() + " HomeworldDefaultSaveDatas.");
+                try {
+                    val sceneId = Integer.parseInt(matcher.group(1));
+                    val data = JsonUtils.loadToClass(path, HomeworldDefaultSaveData.class);
+                    GameData.getHomeworldDefaultSaveData().put(sceneId, data);
+                } catch (Exception ignored) {}
+            });
+            Grasscutter.getLogger().debug("Loaded " + GameData.getHomeworldDefaultSaveData().size() + " HomeworldDefaultSaveDatas.");
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Failed to load HomeworldDefaultSave folder.");
+        }
     }
 
-    @SneakyThrows
     private static void loadNpcBornData() {
-        Files.list(Path.of(RESOURCE("BinOutput/Scene/SceneNpcBorn"))).forEach(file -> {
-            if (file.toFile().isDirectory()) {
-                return;
-            }
-            try {
-                var data = JsonUtils.loadToClass(file.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);
-            } catch (Exception ignored) {}
-        });
+        try {
+            Files.newDirectoryStream(getResourcePath("BinOutput/Scene/SceneNpcBorn/"), "*.json").forEach(path -> {
+                try {
+                    val data = JsonUtils.loadToClass(path, SceneNpcBornData.class);
+                    if (data.getBornPosList() == null || data.getBornPosList().size() == 0) {
+                        return;
+                    }
 
-        Grasscutter.getLogger().debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas.");
+                    data.setIndex(SceneIndexManager.buildIndex(3, data.getBornPosList(), item -> item.getPos().toPoint()));
+                    GameData.getSceneNpcBornData().put(data.getSceneId(), data);
+                } catch (IOException ignored) {}
+            });
+            Grasscutter.getLogger().debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas.");
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Failed to load SceneNpcBorn folder.");
+        }
     }
 
-    @SneakyThrows
     private static void loadGadgetConfigData() {
-        Files.list(Path.of(RESOURCE("BinOutput/Gadget/"))).forEach(filePath -> {
-            var file = filePath.toFile();
-            if (file.isDirectory() || !file.getName().endsWith("json")) {
-                return;
-            }
-
-            Map<String, ConfigGadget> config;
-
-            try {
-                config = JsonUtils.loadToMap(filePath.toString(), String.class, ConfigGadget.class);
-            } catch (Exception e) {
-                Grasscutter.getLogger().error("failed to load ConfigGadget entries for "+filePath, e);
-                return;
-            }
-
-            for (Entry<String, ConfigGadget> e : config.entrySet()) {
-                GameData.getGadgetConfigData().put(e.getKey(), e.getValue());
-            }
-        });
+        try {
+            Files.newDirectoryStream(getResourcePath("BinOutput/Gadget/"), "*.json").forEach(path -> {
+                try {
+                    GameData.getGadgetConfigData().putAll(JsonUtils.loadToMap(path, String.class, ConfigGadget.class));
+                } catch (Exception e) {
+                    Grasscutter.getLogger().error("failed to load ConfigGadget entries for " + path.toString(), e);
+                    return;
+                }
+            });
 
-        Grasscutter.getLogger().debug("Loaded {} ConfigGadget entries.", GameData.getGadgetConfigData().size());
+            Grasscutter.getLogger().debug("Loaded {} ConfigGadget entries.", GameData.getGadgetConfigData().size());
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Failed to load ConfigGadget folder.");
+        }
     }
 
-    @SneakyThrows
     private static void loadBlossomResources() {
-        GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class));
-        Grasscutter.getLogger().debug("Loaded BlossomConfig.");
+        try {
+            GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class));
+            Grasscutter.getLogger().debug("Loaded BlossomConfig.");
+        } catch (IOException e) {
+            Grasscutter.getLogger().warn("Failed to load BlossomConfig.");
+        }
     }
 
     // BinOutput configs
diff --git a/src/main/java/emu/grasscutter/tools/Tools.java b/src/main/java/emu/grasscutter/tools/Tools.java
index 792b8e77..0823afaf 100644
--- a/src/main/java/emu/grasscutter/tools/Tools.java
+++ b/src/main/java/emu/grasscutter/tools/Tools.java
@@ -2,9 +2,11 @@ package emu.grasscutter.tools;
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -215,11 +217,15 @@ public final class Tools {
     }
 
     public static List<String> getAvailableLanguage() {
-        File textMapFolder = new File(RESOURCE("TextMap"));
         List<String> availableLangList = new ArrayList<>();
-        for (String textMapFileName : Objects.requireNonNull(textMapFolder.list((dir, name) -> name.startsWith("TextMap") && name.endsWith(".json")))) {
-            availableLangList.add(textMapFileName.replace("TextMap", "").replace(".json", "").toLowerCase());
-        } return availableLangList;
+        try {
+            Files.newDirectoryStream(getResourcePath("TextMap"), "TextMap*.json").forEach(path -> {
+                availableLangList.add(path.getFileName().toString().replace("TextMap", "").replace(".json", "").toLowerCase());
+            });
+        } catch (IOException e) {
+            Grasscutter.getLogger().error("Failed to get available languages:", e);
+        }
+        return availableLangList;
     }
 
     @Deprecated(forRemoval = true, since = "1.2.3")
diff --git a/src/main/java/emu/grasscutter/utils/FileUtils.java b/src/main/java/emu/grasscutter/utils/FileUtils.java
index ff3c23e8..6ef4f776 100644
--- a/src/main/java/emu/grasscutter/utils/FileUtils.java
+++ b/src/main/java/emu/grasscutter/utils/FileUtils.java
@@ -9,7 +9,6 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -17,7 +16,7 @@ import java.util.stream.Collectors;
 
 public final class FileUtils {
 	public static void write(String dest, byte[] bytes) {
-		Path path = Paths.get(dest);
+		Path path = Path.of(dest);
 		
 		try {
 			Files.write(path, bytes);
@@ -27,7 +26,7 @@ public final class FileUtils {
 	}
 	
 	public static byte[] read(String dest) {
-		return read(Paths.get(dest));
+		return read(Path.of(dest));
 	}
 
 	public static byte[] read(Path path) {
diff --git a/src/main/java/emu/grasscutter/utils/JsonUtils.java b/src/main/java/emu/grasscutter/utils/JsonUtils.java
index ea2b3483..5742b87f 100644
--- a/src/main/java/emu/grasscutter/utils/JsonUtils.java
+++ b/src/main/java/emu/grasscutter/utils/JsonUtils.java
@@ -3,7 +3,10 @@ package emu.grasscutter.utils;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.Reader;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 
@@ -32,36 +35,57 @@ public final class JsonUtils {
         return gson.fromJson(jsonElement, classType);
     }
 
-    public static <T> T loadToClass(InputStreamReader fileReader, Class<T> classType) throws IOException {
+    public static <T> T loadToClass(Reader fileReader, Class<T> classType) throws IOException {
         return gson.fromJson(fileReader, classType);
     }
 
+    @Deprecated
     public static <T> T loadToClass(String filename, Class<T> classType) throws IOException {
         try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
             return loadToClass(fileReader, classType);
         }
     }
 
-    public static <T> List<T> loadToList(InputStreamReader fileReader, Class<T> classType) throws IOException {
+    public static <T> T loadToClass(Path filename, Class<T> classType) throws IOException {
+        try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) {
+            return loadToClass(fileReader, classType);
+        }
+    }
+
+    public static <T> List<T> loadToList(Reader fileReader, Class<T> classType) throws IOException {
         return gson.fromJson(fileReader, TypeToken.getParameterized(List.class, classType).getType());
     }
 
+    @Deprecated
     public static <T> List<T> loadToList(String filename, Class<T> classType) throws IOException {
         try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
             return loadToList(fileReader, classType);
         }
     }
 
-    public static <T1,T2> Map<T1,T2> loadToMap(InputStreamReader fileReader, Class<T1> keyType, Class<T2> valueType) throws IOException {
+    public static <T> List<T> loadToList(Path filename, Class<T> classType) throws IOException {
+        try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) {
+            return loadToList(fileReader, classType);
+        }
+    }
+
+    public static <T1,T2> Map<T1,T2> loadToMap(Reader fileReader, Class<T1> keyType, Class<T2> valueType) throws IOException {
         return gson.fromJson(fileReader, TypeToken.getParameterized(Map.class, keyType, valueType).getType());
     }
 
+    @Deprecated
     public static <T1,T2> Map<T1,T2> loadToMap(String filename, Class<T1> keyType, Class<T2> valueType) throws IOException {
         try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(Utils.toFilePath(filename)), StandardCharsets.UTF_8)) {
             return loadToMap(fileReader, keyType, valueType);
         }
     }
 
+    public static <T1,T2> Map<T1,T2> loadToMap(Path filename, Class<T1> keyType, Class<T2> valueType) throws IOException {
+        try (var fileReader = Files.newBufferedReader(filename, StandardCharsets.UTF_8)) {
+            return loadToMap(fileReader, keyType, valueType);
+        }
+    }
+
     /**
      * Safely JSON decodes a given string.
      * @param jsonData The JSON-encoded data.
diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java
index c06a7c96..f82fdb16 100644
--- a/src/main/java/emu/grasscutter/utils/Language.java
+++ b/src/main/java/emu/grasscutter/utils/Language.java
@@ -325,7 +325,7 @@ public final class Language {
 
     private static Int2ObjectMap<String> loadTextMapFile(String language, IntSet nameHashes) {
         Int2ObjectMap<String> output = new Int2ObjectOpenHashMap<>();
-        try (BufferedReader file = new BufferedReader(new FileReader(Utils.toFilePath(RESOURCE("TextMap/TextMap"+language+".json")), StandardCharsets.UTF_8))) {
+        try (BufferedReader file = Files.newBufferedReader(getResourcePath("TextMap/TextMap"+language+".json"), StandardCharsets.UTF_8)) {
             Matcher matcher = textMapKeyValueRegex.matcher("");
             return new Int2ObjectOpenHashMap<>(
                 file.lines()
@@ -406,7 +406,7 @@ public final class Language {
         try {
             long cacheModified = Files.getLastModifiedTime(TEXTMAP_CACHE_PATH).toMillis();
 
-            long textmapsModified = Files.list(Path.of(RESOURCE("TextMap")))
+            long textmapsModified = Files.list(getResourcePath("TextMap"))
                 .filter(path -> path.toString().endsWith(".json"))
                 .map(path -> {
                     try {
diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java
index bc09a6e6..9ae7f95b 100644
--- a/src/main/java/emu/grasscutter/utils/Utils.java
+++ b/src/main/java/emu/grasscutter/utils/Utils.java
@@ -22,6 +22,7 @@ import org.slf4j.Logger;
 
 import javax.annotation.Nullable;
 
+import static emu.grasscutter.config.Configuration.getResourcePath;
 import static emu.grasscutter.utils.Language.translate;
 
 @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
@@ -169,19 +170,18 @@ public final class Utils {
         Logger logger = Grasscutter.getLogger();
         boolean exit = false;
 
-        String resourcesFolder = config.folderStructure.resources;
         String dataFolder = config.folderStructure.data;
 
         // Check for resources folder.
-        if (!fileExists(resourcesFolder)) {
+        if (!Files.exists(getResourcePath(""))) {
             logger.info(translate("messages.status.create_resources"));
             logger.info(translate("messages.status.resources_error"));
-            createFolder(resourcesFolder); exit = true;
+            createFolder(config.folderStructure.resources); exit = true;
         }
 
         // Check for BinOutput + ExcelBinOutput.
-        if (!fileExists(resourcesFolder + "BinOutput") ||
-                !fileExists(resourcesFolder + "ExcelBinOutput")) {
+        if (!Files.exists(getResourcePath("BinOutput")) ||
+            !Files.exists(getResourcePath("ExcelBinOutput"))) {
             logger.info(translate("messages.status.resources_error"));
             exit = true;
         }
-- 
GitLab