package emu.grasscutter.server.http.documentation; import static emu.grasscutter.config.Configuration.*; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.excels.AvatarData; 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.HttpUtils; import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Utils; import io.javalin.http.ContentType; import io.javalin.http.Context; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; final class HandbookRequestHandler implements DocumentationHandler { private List handbookHtmls; private final String template; public HandbookRequestHandler() { final File templateFile = new File(Utils.toFilePath(DATA("documentation/handbook.html"))); if (templateFile.exists()) { this.template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8); this.handbookHtmls = generateHandbookHtmls(); } else { Grasscutter.getLogger().warn("File does not exist: " + templateFile); this.template = null; } } @Override public void handle(Context ctx) { 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) { ctx.status(500); } else { ctx.contentType(ContentType.TEXT_HTML); ctx.result(handbookHtmls.get(langIdx)); } } 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 CommandMap.getInstance().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. 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}}", 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; } }