package emu.grasscutter.server.http.documentation; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; import emu.grasscutter.data.GameData; import emu.grasscutter.data.excels.ItemData; import emu.grasscutter.data.excels.avatar.AvatarData; import emu.grasscutter.data.excels.monster.MonsterData; import emu.grasscutter.data.excels.scene.SceneData; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.lang.Language; import io.javalin.http.ContentType; import io.javalin.http.Context; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; final class HandbookRequestHandler implements DocumentationHandler { private List handbookHtmls; public HandbookRequestHandler() { var templatePath = FileUtils.getDataPath("documentation/handbook.html"); try { this.handbookHtmls = generateHandbookHtmls(Files.readString(templatePath)); } catch (IOException ignored) { Grasscutter.getLogger().warn("File does not exist: " + templatePath); } } @Override public void handle(Context ctx) { int langIdx = 0; String acceptLanguage = ctx.header("Accept-Language"); if (acceptLanguage != null) { Pattern localePattern = Pattern.compile("[a-z]+-[A-Z]+"); Matcher matcher = localePattern.matcher(acceptLanguage); if (matcher.find()) { String lang = matcher.group(0); langIdx = Language.TextStrings.MAP_GC_LANGUAGES.getOrDefault(lang, 0); } } if (this.handbookHtmls == null) { ctx.status(500); } else { if (langIdx <= this.handbookHtmls.size() - 1) { ctx.contentType(ContentType.TEXT_HTML); ctx.result(this.handbookHtmls.get(langIdx)); } } } private List generateHandbookHtmls(String template) { 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; } }