diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index d57301e85..633dd9cfb 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 3fc081a9c..b7a0d94dd 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 8457b7e0e..d7c1eb6ff 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 map; + private List 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>() { - }.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 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 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 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 8c8c749cb..d2ca07acd 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 handbookHtmls; private final String template; - private Map 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>() { - }.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)); } + } - final CommandMap cmdMap = new CommandMap(true); + private List generateHandbookHtmls() { + final int NUM_LANGUAGES = Language.TextStrings.NUM_LANGUAGES; + final List output = new ArrayList<>(NUM_LANGUAGES); + final List languages = Language.TextStrings.getLanguages(); + final List sbs = new ArrayList<>(NUM_LANGUAGES); + for (int langIdx = 0; langIdx < NUM_LANGUAGES; langIdx++) + sbs.add(new StringBuilder("")); + + // 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("" + label + "" + languages.get(langIdx).get(descKey) + "\n"); + }); + sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n + final List cmdsTable = sbs.stream().map(StringBuilder::toString).toList(); + + // Avatars table final Int2ObjectMap 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("" + id + "" + name.get(langIdx) + "\n"); + }); + sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n + final List avatarsTable = sbs.stream().map(StringBuilder::toString).toList(); + + // Items table final Int2ObjectMap 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("" + id + "" + name.get(langIdx) + "\n"); + }); + sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n + final List itemsTable = sbs.stream().map(StringBuilder::toString).toList(); + + // Scenes table final Int2ObjectMap 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("" + id + "" + data.getScriptData() + "\n"); + }); + sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n + final List scenesTable = sbs.stream().map(StringBuilder::toString).toList(); + + // Monsters table final Int2ObjectMap 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("" + id + "" + name.get(langIdx) + "\n"); + }); + sbs.forEach(sb -> sb.setLength(sb.length()-1)); // Remove trailing \n + final List 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 -> "" + cmd.getLabel() + "" + - cmd.getDescriptionString(null) + "") - .collect(Collectors.joining("\n"))) - // Avatars table - .replace("{{AVATARS_TABLE}}", GameData.getAvatarDataMap().keySet() - .intStream() - .sorted() - .mapToObj(avatarMap::get) - .map(data -> "" + data.getId() + "" + - map.get(data.getNameTextMapHash()) + "") - .collect(Collectors.joining("\n"))) - // Items table - .replace("{{ITEMS_TABLE}}", GameData.getItemDataMap().keySet() - .intStream() - .sorted() - .mapToObj(itemMap::get) - .map(data -> "" + data.getId() + "" + - map.get(data.getNameTextMapHash()) + "") - .collect(Collectors.joining("\n"))) - // Scenes table - .replace("{{SCENES_TABLE}}", GameData.getSceneDataMap().keySet() - .intStream() - .sorted() - .mapToObj(sceneMap::get) - .map(data -> "" + data.getId() + "" + - data.getScriptData() + "") - .collect(Collectors.joining("\n"))) - .replace("{{MONSTERS_TABLE}}", GameData.getMonsterDataMap().keySet() - .intStream() - .sorted() - .mapToObj(monsterMap::get) - .map(data -> "" + data.getId() + "" + - map.get(data.getNameTextMapHash()) + "") - .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 3060906da..a031683fd 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 LIST_LANGUAGES = Arrays.asList(ARR_LANGUAGES); public static final Object2IntMap MAP_LANGUAGES = // Map "EN": 0, "CHS": 1, ..., "VI": 12 @@ -276,6 +276,14 @@ public final class Language { } } + public static List 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)]; }