From c1ff7332fe80ffca55915973000b57c4b21b5736 Mon Sep 17 00:00:00 2001
From: AnimeGitB <AnimeGitB@bigblueball.in>
Date: Wed, 3 Aug 2022 17:11:04 +0930
Subject: [PATCH] Generate handbooks on every launch, fix html docs

---
 .../java/emu/grasscutter/Grasscutter.java     |   2 +-
 .../grasscutter/command/CommandHandler.java   |  11 +-
 .../GachaMappingRequestHandler.java           | 167 +++++++----------
 .../documentation/HandbookRequestHandler.java | 171 ++++++++++--------
 .../java/emu/grasscutter/utils/Language.java  |  10 +-
 5 files changed, 176 insertions(+), 185 deletions(-)

diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java
index d57301e8..633dd9cf 100644
--- a/src/main/java/emu/grasscutter/Grasscutter.java
+++ b/src/main/java/emu/grasscutter/Grasscutter.java
@@ -89,13 +89,13 @@ public final class Grasscutter {
 
     public static void main(String[] args) throws Exception {
         Crypto.loadKeys(); // Load keys from buffers.
+        Tools.createGmHandbooks();
 
         // Parse arguments.
         boolean exitEarly = false;
         for (String arg : args) {
             switch (arg.toLowerCase()) {
                 case "-handbook", "-handbooks" -> {
-                    Tools.createGmHandbooks();
                     exitEarly = true;
                 }
                 case "-dumppacketids" -> {
diff --git a/src/main/java/emu/grasscutter/command/CommandHandler.java b/src/main/java/emu/grasscutter/command/CommandHandler.java
index 3fc081a9..b7a0d94d 100644
--- a/src/main/java/emu/grasscutter/command/CommandHandler.java
+++ b/src/main/java/emu/grasscutter/command/CommandHandler.java
@@ -3,6 +3,8 @@ package emu.grasscutter.command;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.game.player.Player;
 import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
+import emu.grasscutter.utils.Language;
+
 import static emu.grasscutter.utils.Language.translate;
 
 import java.util.List;
@@ -68,10 +70,13 @@ public interface CommandHandler {
         return this.getClass().getAnnotation(Command.class).label();
     }
 
-    default String getDescriptionString(Player player) {
+    default String getDescriptionKey() {
         Command annotation = this.getClass().getAnnotation(Command.class);
-        String key = "commands.%s.description".formatted(annotation.label());
-        return translate(player, key);
+        return "commands.%s.description".formatted(annotation.label());
+    }
+
+    default String getDescriptionString(Player player) {
+        return translate(player, getDescriptionKey());
     }
 
     /**
diff --git a/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java
index 8457b7e0..d7c1eb6f 100644
--- a/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java
+++ b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java
@@ -1,154 +1,117 @@
 package emu.grasscutter.server.http.documentation;
 
-import com.google.gson.reflect.TypeToken;
-import emu.grasscutter.Grasscutter;
 import emu.grasscutter.data.GameData;
 import emu.grasscutter.data.excels.AvatarData;
 import emu.grasscutter.data.excels.ItemData;
-import emu.grasscutter.utils.Utils;
+import emu.grasscutter.utils.Language;
 import express.http.Request;
 import express.http.Response;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 
 import static emu.grasscutter.config.Configuration.DOCUMENT_LANGUAGE;
-import static emu.grasscutter.config.Configuration.RESOURCE;
 
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 final class GachaMappingRequestHandler implements DocumentationHandler {
-
-    private Map<Long, String> map;
+    private List<String> gachaJsons;
 
     GachaMappingRequestHandler() {
-        final String textMapFile = "TextMap/TextMap" + DOCUMENT_LANGUAGE + ".json";
-        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
-                Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
-            map = Grasscutter.getGsonFactory().fromJson(fileReader,
-                    new TypeToken<Map<Long, String>>() {
-                    }.getType());
-        } catch (IOException e) {
-            Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
-            map = new HashMap<>();
-        }
+        this.gachaJsons = createGachaMappingJsons();
     }
 
     @Override
     public void handle(Request request, Response response) {
-        if (map.isEmpty()) {
-            response.status(500);
-        } else {
-            response.set("Content-Type", "application/json")
-                    .ctx()
-                    .result(createGachaMappingJson());
-        }
+        final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0);  // TODO: This should really be based off the client language somehow
+        response.set("Content-Type", "application/json")
+                .ctx()
+                .result(gachaJsons.get(langIdx));
     }
 
-    private String createGachaMappingJson() {
-        List<Integer> list;
-
-        final StringBuilder sb = new StringBuilder();
-        list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
-        Collections.sort(list);
-
-        final String newLine = System.lineSeparator();
-
-        // if the user made choices for language, I assume it's okay to assign his/her selected language to "en-us"
-        // since it's the fallback language and there will be no difference in the gacha record page.
-        // The enduser can still modify the `gacha_mappings.js` directly to enable multilingual for the gacha record system.
-        sb.append("{").append(newLine);
+    private List<String> createGachaMappingJsons() {
+        final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES;
+        final Language.TextStrings CHARACTER = Language.getTextMapKey(4233146695L);  // "Character" in EN
+        final Language.TextStrings WEAPON = Language.getTextMapKey(4231343903L);  // "Weapon" in EN
+        final Language.TextStrings STANDARD_WISH = Language.getTextMapKey(332935371L);  // "Standard Wish" in EN
+        final Language.TextStrings CHARACTER_EVENT_WISH = Language.getTextMapKey(2272170627L);  // "Character Event Wish" in EN
+        final Language.TextStrings CHARACTER_EVENT_WISH_2 = Language.getTextMapKey(3352513147L);  // "Character Event Wish-2" in EN
+        final Language.TextStrings WEAPON_EVENT_WISH = Language.getTextMapKey(2864268523L);  // "Weapon Event Wish" in EN
+        final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES);
+        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+            sbs.add(new StringBuilder("{\n"));  // Web requests should never need Windows line endings
 
         // Avatars
-        boolean first = true;
-        for (Integer id : list) {
+        IntList list = new IntArrayList(GameData.getAvatarDataMap().keySet().intStream().sorted().toArray());
+        for (int id : list) {
             AvatarData data = GameData.getAvatarDataMap().get(id);
             int avatarID = data.getId();
             if (avatarID >= 11000000) { // skip test avatar
                 continue;
             }
-            if (first) { // skip adding comma for the first element
-                first = false;
-            } else {
-                sb.append(",");
-            }
-            String color;
-            switch (data.getQualityType()) {
-                case "QUALITY_PURPLE":
-                    color = "purple";
-                    break;
-                case "QUALITY_ORANGE":
-                    color = "yellow";
-                    break;
-                case "QUALITY_BLUE":
-                default:
-                    color = "blue";
-            }
-            // Got the magic number 4233146695 from manually search in the json file
-            sb.append("\"")
+            String color = switch (data.getQualityType()) {
+                case "QUALITY_PURPLE" -> "purple";
+                case "QUALITY_ORANGE" -> "yellow";
+                case "QUALITY_BLUE" -> "blue";
+                default -> "";
+            };
+            Language.TextStrings avatarName = Language.getTextMapKey(data.getNameTextMapHash());
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
+                sbs.get(langIdx)
+                    .append("\"")
                     .append(avatarID % 1000 + 1000)
                     .append("\" : [\"")
-                    .append(map.get(data.getNameTextMapHash()))
+                    .append(avatarName.get(langIdx))
                     .append("(")
-                    .append(map.get(4233146695L))
+                    .append(CHARACTER.get(langIdx))
                     .append(")\", \"")
                     .append(color)
-                    .append("\"]")
-                    .append(newLine);
+                    .append("\"],\n");
+            }
         }
 
-        list = new ArrayList<>(GameData.getItemDataMap().keySet());
-        Collections.sort(list);
+        list = new IntArrayList(GameData.getItemDataMap().keySet().intStream().sorted().toArray());
 
         // Weapons
-        for (Integer id : list) {
+        for (int id : list) {
             ItemData data = GameData.getItemDataMap().get(id);
             if (data.getId() <= 11101 || data.getId() >= 20000) {
                 continue; //skip non weapon items
             }
-            String color;
-
-            switch (data.getRankLevel()) {
-                case 3:
-                    color = "blue";
-                    break;
-                case 4:
-                    color = "purple";
-                    break;
-                case 5:
-                    color = "yellow";
-                    break;
-                default:
-                    continue; // skip unnecessary entries
-            }
-
-            // Got the magic number 4231343903 from manually search in the json file
-
-            sb.append(",\"")
+            String color = switch (data.getRankLevel()) {
+                case 3 -> "blue";
+                case 4 -> "purple";
+                case 5 -> "yellow";
+                default -> null;
+            };
+            if (color == null) continue;  // skip unnecessary entries
+            Language.TextStrings weaponName = Language.getTextMapKey(data.getNameTextMapHash());
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
+                sbs.get(langIdx)
+                    .append("\"")
                     .append(data.getId())
                     .append("\" : [\"")
-                    .append(map.get(data.getNameTextMapHash()).replaceAll("\"", ""))
+                    .append(weaponName.get(langIdx).replaceAll("\"", "\\\\\""))
                     .append("(")
-                    .append(map.get(4231343903L))
+                    .append(WEAPON.get(langIdx))
                     .append(")\",\"")
                     .append(color)
-                    .append("\"]")
-                    .append(newLine);
+                    .append("\"],\n");
+            }
         }
-        sb.append(",\"200\": \"")
-                .append(map.get(332935371L))
+
+        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
+            sbs.get(langIdx)
+                .append("\"200\": \"")
+                .append(STANDARD_WISH.get(langIdx))
                 .append("\", \"301\": \"")
-                .append(map.get(2272170627L))
+                .append(CHARACTER_EVENT_WISH.get(langIdx))
+                .append("\", \"400\": \"")
+                .append(CHARACTER_EVENT_WISH_2.get(langIdx))
                 .append("\", \"302\": \"")
-                .append(map.get(2864268523L))
-                .append("\"")
-                .append("}\n}")
-                .append(newLine);
-        return sb.toString();
+                .append(WEAPON_EVENT_WISH.get(langIdx))
+                .append("\"\n}\n");
+        }
+        return sbs.stream().map(StringBuilder::toString).toList();
     }
 }
diff --git a/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java
index 8c8c749c..d2ca07ac 100644
--- a/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java
+++ b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java
@@ -1,9 +1,7 @@
 package emu.grasscutter.server.http.documentation;
 
 import static emu.grasscutter.config.Configuration.*;
-import static emu.grasscutter.utils.Language.translate;
 
-import com.google.gson.reflect.TypeToken;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.command.CommandMap;
 import emu.grasscutter.data.GameData;
@@ -12,112 +10,129 @@ import emu.grasscutter.data.excels.ItemData;
 import emu.grasscutter.data.excels.MonsterData;
 import emu.grasscutter.data.excels.SceneData;
 import emu.grasscutter.utils.FileUtils;
+import emu.grasscutter.utils.Language;
 import emu.grasscutter.utils.Utils;
 import express.http.Request;
 import express.http.Response;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.List;
 
 final class HandbookRequestHandler implements DocumentationHandler {
-
+    private List<String> handbookHtmls;
     private final String template;
-    private Map<Long, String> map;
-
 
     public HandbookRequestHandler() {
         final File templateFile = new File(Utils.toFilePath(DATA("documentation/handbook.html")));
         if (templateFile.exists()) {
-            template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
+            this.template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8);
+            this.handbookHtmls = generateHandbookHtmls();
         } else {
             Grasscutter.getLogger().warn("File does not exist: " + templateFile);
-            template = null;
-        }
-
-        final String textMapFile = "TextMap/TextMap" + DOCUMENT_LANGUAGE + ".json";
-        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(
-                Utils.toFilePath(RESOURCE(textMapFile))), StandardCharsets.UTF_8)) {
-            map = Grasscutter.getGsonFactory()
-                    .fromJson(fileReader, new TypeToken<Map<Long, String>>() {
-                    }.getType());
-        } catch (IOException e) {
-            Grasscutter.getLogger().warn("Resource does not exist: " + textMapFile);
-            map = new HashMap<>();
+            this.template = null;
         }
     }
 
     @Override
     public void handle(Request request, Response response) {
+        final int langIdx = Language.TextStrings.MAP_LANGUAGES.getOrDefault(DOCUMENT_LANGUAGE, 0);  // TODO: This should really be based off the client language somehow
         if (template == null) {
             response.status(500);
-            return;
+        } else {
+            response.send(handbookHtmls.get(langIdx));
         }
+    }
+
+    private List<String> generateHandbookHtmls() {
+        final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES;
+        final List<String> output = new ArrayList<>(NUM_LANGUAGES);
+        final List<Language> languages = Language.TextStrings.getLanguages();
+        final List<StringBuilder> sbs = new ArrayList<>(NUM_LANGUAGES);
+        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+            sbs.add(new StringBuilder(""));
 
-        final CommandMap cmdMap = new CommandMap(true);
+        // Commands table
+        new CommandMap(true).getHandlersAsList().forEach(cmd -> {
+            String label = cmd.getLabel();
+            String descKey = cmd.getDescriptionKey();
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+                sbs.get(langIdx).append("<tr><td><code>" + label + "</code></td><td>" + languages.get(langIdx).get(descKey) + "</td></tr>\n");
+        });
+        sbs.forEach(sb -> sb.setLength(sb.length()-1));  // Remove trailing \n
+        final List<String> cmdsTable = sbs.stream().map(StringBuilder::toString).toList();
+
+        // Avatars table
         final Int2ObjectMap<AvatarData> avatarMap = GameData.getAvatarDataMap();
+        sbs.forEach(sb -> sb.setLength(0));
+        avatarMap.keySet().intStream().sorted().mapToObj(avatarMap::get).forEach(data -> {
+            int id = data.getId();
+            Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+                sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
+        });
+        sbs.forEach(sb -> sb.setLength(sb.length()-1));  // Remove trailing \n
+        final List<String> avatarsTable = sbs.stream().map(StringBuilder::toString).toList();
+
+        // Items table
         final Int2ObjectMap<ItemData> itemMap = GameData.getItemDataMap();
+        sbs.forEach(sb -> sb.setLength(0));
+        itemMap.keySet().intStream().sorted().mapToObj(itemMap::get).forEach(data -> {
+            int id = data.getId();
+            Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+                sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
+        });
+        sbs.forEach(sb -> sb.setLength(sb.length()-1));  // Remove trailing \n
+        final List<String> itemsTable = sbs.stream().map(StringBuilder::toString).toList();
+
+        // Scenes table
         final Int2ObjectMap<SceneData> sceneMap = GameData.getSceneDataMap();
+        sceneMap.keySet().intStream().sorted().mapToObj(sceneMap::get).forEach(data -> {
+            int id = data.getId();
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+                sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + data.getScriptData() + "</td></tr>\n");
+        });
+        sbs.forEach(sb -> sb.setLength(sb.length()-1));  // Remove trailing \n
+        final List<String> scenesTable = sbs.stream().map(StringBuilder::toString).toList();
+
+        // Monsters table
         final Int2ObjectMap<MonsterData> monsterMap = GameData.getMonsterDataMap();
+        monsterMap.keySet().intStream().sorted().mapToObj(monsterMap::get).forEach(data -> {
+            int id = data.getId();
+            Language.TextStrings name = Language.getTextMapKey(data.getNameTextMapHash());
+            for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++)
+                sbs.get(langIdx).append("<tr><td><code>" + id + "</code></td><td>" + name.get(langIdx) + "</td></tr>\n");
+        });
+        sbs.forEach(sb -> sb.setLength(sb.length()-1));  // Remove trailing \n
+        final List<String> monstersTable = sbs.stream().map(StringBuilder::toString).toList();
 
         // Add translated title etc. to the page.
-        String content = template.replace("{{TITLE}}", translate("documentation.handbook.title"))
-                .replace("{{TITLE_COMMANDS}}", translate("documentation.handbook.title_commands"))
-                .replace("{{TITLE_AVATARS}}", translate("documentation.handbook.title_avatars"))
-                .replace("{{TITLE_ITEMS}}", translate("documentation.handbook.title_items"))
-                .replace("{{TITLE_SCENES}}", translate("documentation.handbook.title_scenes"))
-                .replace("{{TITLE_MONSTERS}}", translate("documentation.handbook.title_monsters"))
-                .replace("{{HEADER_ID}}", translate("documentation.handbook.header_id"))
-                .replace("{{HEADER_COMMAND}}", translate("documentation.handbook.header_command"))
-                .replace("{{HEADER_DESCRIPTION}}",
-                        translate("documentation.handbook.header_description"))
-                .replace("{{HEADER_AVATAR}}", translate("documentation.handbook.header_avatar"))
-                .replace("{{HEADER_ITEM}}", translate("documentation.handbook.header_item"))
-                .replace("{{HEADER_SCENE}}", translate("documentation.handbook.header_scene"))
-                .replace("{{HEADER_MONSTER}}", translate("documentation.handbook.header_monster"))
+        for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) {
+            Language lang = languages.get(langIdx);
+            output.add(template
+                .replace("{{TITLE}}", lang.get("documentation.handbook.title"))
+                .replace("{{TITLE_COMMANDS}}", lang.get("documentation.handbook.title_commands"))
+                .replace("{{TITLE_AVATARS}}", lang.get("documentation.handbook.title_avatars"))
+                .replace("{{TITLE_ITEMS}}", lang.get("documentation.handbook.title_items"))
+                .replace("{{TITLE_SCENES}}", lang.get("documentation.handbook.title_scenes"))
+                .replace("{{TITLE_MONSTERS}}", lang.get("documentation.handbook.title_monsters"))
+                .replace("{{HEADER_ID}}", lang.get("documentation.handbook.header_id"))
+                .replace("{{HEADER_COMMAND}}", lang.get("documentation.handbook.header_command"))
+                .replace("{{HEADER_DESCRIPTION}}", lang.get("documentation.handbook.header_description"))
+                .replace("{{HEADER_AVATAR}}", lang.get("documentation.handbook.header_avatar"))
+                .replace("{{HEADER_ITEM}}", lang.get("documentation.handbook.header_item"))
+                .replace("{{HEADER_SCENE}}", lang.get("documentation.handbook.header_scene"))
+                .replace("{{HEADER_MONSTER}}", lang.get("documentation.handbook.header_monster"))
                 // Commands table
-                .replace("{{COMMANDS_TABLE}}", cmdMap.getHandlersAsList()
-                        .stream()
-                        .map(cmd -> "<tr><td><code>" + cmd.getLabel() + "</code></td><td>" +
-                                cmd.getDescriptionString(null) + "</td></tr>")
-                        .collect(Collectors.joining("\n")))
-                // Avatars table
-                .replace("{{AVATARS_TABLE}}", GameData.getAvatarDataMap().keySet()
-                        .intStream()
-                        .sorted()
-                        .mapToObj(avatarMap::get)
-                        .map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
-                                map.get(data.getNameTextMapHash()) + "</td></tr>")
-                        .collect(Collectors.joining("\n")))
-                // Items table
-                .replace("{{ITEMS_TABLE}}", GameData.getItemDataMap().keySet()
-                        .intStream()
-                        .sorted()
-                        .mapToObj(itemMap::get)
-                        .map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
-                                map.get(data.getNameTextMapHash()) + "</td></tr>")
-                        .collect(Collectors.joining("\n")))
-                // Scenes table
-                .replace("{{SCENES_TABLE}}", GameData.getSceneDataMap().keySet()
-                        .intStream()
-                        .sorted()
-                        .mapToObj(sceneMap::get)
-                        .map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
-                                data.getScriptData() + "</td></tr>")
-                        .collect(Collectors.joining("\n")))
-                .replace("{{MONSTERS_TABLE}}", GameData.getMonsterDataMap().keySet()
-                        .intStream()
-                        .sorted()
-                        .mapToObj(monsterMap::get)
-                        .map(data -> "<tr><td><code>" + data.getId() + "</code></td><td>" +
-                                map.get(data.getNameTextMapHash()) + "</td></tr>")
-                        .collect(Collectors.joining("\n")));
-
-        response.send(content);
+                .replace("{{COMMANDS_TABLE}}", cmdsTable.get(langIdx))
+                .replace("{{AVATARS_TABLE}}", avatarsTable.get(langIdx))
+                .replace("{{ITEMS_TABLE}}", itemsTable.get(langIdx))
+                .replace("{{SCENES_TABLE}}", scenesTable.get(langIdx))
+                .replace("{{MONSTERS_TABLE}}", monstersTable.get(langIdx))
+            );
+        }
+        return output;
     }
 }
diff --git a/src/main/java/emu/grasscutter/utils/Language.java b/src/main/java/emu/grasscutter/utils/Language.java
index 3060906d..a031683f 100644
--- a/src/main/java/emu/grasscutter/utils/Language.java
+++ b/src/main/java/emu/grasscutter/utils/Language.java
@@ -240,7 +240,7 @@ public final class Language {
     private static final int TEXTMAP_CACHE_VERSION = 0x9CCACE02;
     @EqualsAndHashCode public static class TextStrings implements Serializable {
         public static final String[] ARR_LANGUAGES = {"EN", "CHS", "CHT", "JP", "KR", "DE", "ES", "FR", "ID", "PT", "RU", "TH", "VI"};
-        public static final String[] ARR_GC_LANGUAGES = {"en-US", "zh-CN", "zh-TW", "JP", "KR", "DE", "es-ES", "fr-FR", "ID", "PT", "ru-RU", "TH", "VI"};
+        public static final String[] ARR_GC_LANGUAGES = {"en-US", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "DE", "es-ES", "fr-FR", "ID", "PT", "ru-RU", "TH", "VI"};
         public static final int NUM_LANGUAGES = ARR_LANGUAGES.length;
         public static final List<String> LIST_LANGUAGES = Arrays.asList(ARR_LANGUAGES);
         public static final Object2IntMap<String> MAP_LANGUAGES =  // Map "EN": 0, "CHS": 1, ..., "VI": 12
@@ -276,6 +276,14 @@ public final class Language {
             }
         }
 
+        public static List<Language> getLanguages() {
+            return Arrays.stream(ARR_GC_LANGUAGES).map(Language::getLanguage).toList();
+        }
+
+        public String get(int languageIndex) {
+            return strings[languageIndex];
+        }
+
         public String get(String languageCode) {
             return strings[MAP_LANGUAGES.getOrDefault(languageCode, 0)];
         }
-- 
GitLab