diff --git a/data/documentation/handbook.html b/data/documentation/handbook.html new file mode 100644 index 000000000..6a77d3806 --- /dev/null +++ b/data/documentation/handbook.html @@ -0,0 +1,162 @@ + + + + + + + + + GM Handbook + + +
+
+

{{TITLE}}

+ +

{{TITLE_COMMANDS}}

+
+ + + + + + + + {{COMMANDS_TABLE}} +
{{HEADER_COMMAND}}{{HEADER_DESCRIPTION}}
+ +

{{TITLE_AVATARS}}

+
+ + + + + + + + {{AVATARS_TABLE}} +
{{HEADER_ID}}{{HEADER_AVATAR}}
+ +

{{TITLE_ITEMS}}

+
+ + + + + + + + {{ITEMS_TABLE}} +
{{HEADER_ID}}{{HEADER_ITEM}}
+ + +

{{TITLE_SCENES}}

+
+ + + + + + + + {{SCENES_TABLE}} +
{{HEADER_ID}}{{HEADER_SCENE}}
+ +

{{TITLE_MONSTERS}}

+
+ + + + + + + + {{MONSTERS_TABLE}} +
{{HEADER_ID}}{{HEADER_MONSTER}}
+
+
+ + + diff --git a/data/documentation/index.html b/data/documentation/index.html new file mode 100644 index 000000000..f89dbe258 --- /dev/null +++ b/data/documentation/index.html @@ -0,0 +1,106 @@ + + + + + + + + + Documentation + + +
+
+

{{TITLE}}

+ + +
+
+ + + diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index 316c2030e..3cf8363e7 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -14,6 +14,7 @@ import emu.grasscutter.server.http.HttpServer; import emu.grasscutter.server.http.dispatch.DispatchHandler; import emu.grasscutter.server.http.handlers.*; import emu.grasscutter.server.http.dispatch.RegionHandler; +import emu.grasscutter.server.http.documentation.DocumentationServerHandler; import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.Utils; import org.jline.reader.EndOfFileException; @@ -129,6 +130,7 @@ public final class Grasscutter { httpServer.addRouter(AnnouncementsHandler.class); httpServer.addRouter(DispatchHandler.class); httpServer.addRouter(GachaHandler.class); + httpServer.addRouter(DocumentationServerHandler.class); // TODO: find a better place? StaminaManager.initialize(); diff --git a/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java new file mode 100644 index 000000000..de7f543a3 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationHandler.java @@ -0,0 +1,9 @@ +package emu.grasscutter.server.http.documentation; + +import express.http.Request; +import express.http.Response; + +interface DocumentationHandler { + + void handle(Request request, Response response); +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java new file mode 100644 index 000000000..24c8236de --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/DocumentationServerHandler.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.http.documentation; + +import emu.grasscutter.server.http.Router; +import express.Express; +import io.javalin.Javalin; + +public final class DocumentationServerHandler implements Router { + + @Override + public void applyRoutes(Express express, Javalin handle) { + final RootRequestHandler root = new RootRequestHandler(); + final HandbookRequestHandler handbook = new HandbookRequestHandler(); + final GachaMappingRequestHandler gachaMapping = new GachaMappingRequestHandler(); + + express.get("/documentation/handbook", handbook::handle); + express.get("/documentation/gachamapping", gachaMapping::handle); + express.get("/documentation", root::handle); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java new file mode 100644 index 000000000..e405d9b54 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/GachaMappingRequestHandler.java @@ -0,0 +1,155 @@ +package emu.grasscutter.server.http.documentation; + +import static emu.grasscutter.Configuration.RESOURCE; + +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.ResourceLoader; +import emu.grasscutter.data.def.AvatarData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.tools.Tools; +import emu.grasscutter.utils.Utils; +import express.http.Request; +import express.http.Response; +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; + + GachaMappingRequestHandler() { + ResourceLoader.loadResources(); + final String textMapFile = "TextMap/TextMap" + Tools.getLanguageOption() + ".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<>(); + } + } + + @Override + public void handle(Request request, Response response) { + if (map.isEmpty()) { + response.status(500); + } else { + response.set("Content-Type", "application/json") + .ctx() + .result(createGachaMappingJson()); + } + } + + 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); + + // Avatars + boolean first = true; + for (Integer 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("\"") + .append(avatarID % 1000 + 1000) + .append("\" : [\"") + .append(map.get(data.getNameTextMapHash())) + .append("(") + .append(map.get(4233146695L)) + .append(")\", \"") + .append(color) + .append("\"]") + .append(newLine); + } + + list = new ArrayList<>(GameData.getItemDataMap().keySet()); + Collections.sort(list); + + // Weapons + for (Integer 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(",\"") + .append(data.getId()) + .append("\" : [\"") + .append(map.get(data.getNameTextMapHash()).replaceAll("\"", "")) + .append("(") + .append(map.get(4231343903L)) + .append(")\",\"") + .append(color) + .append("\"]") + .append(newLine); + } + sb.append(",\"200\": \"") + .append(map.get(332935371L)) + .append("\", \"301\": \"") + .append(map.get(2272170627L)) + .append("\", \"302\": \"") + .append(map.get(2864268523L)) + .append("\"") + .append("}\n}") + .append(newLine); + return sb.toString(); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java new file mode 100644 index 000000000..65250d12c --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/HandbookRequestHandler.java @@ -0,0 +1,127 @@ +package emu.grasscutter.server.http.documentation; + +import static emu.grasscutter.Configuration.DATA; +import static emu.grasscutter.Configuration.RESOURCE; +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; +import emu.grasscutter.data.ResourceLoader; +import emu.grasscutter.data.def.AvatarData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.data.def.MonsterData; +import emu.grasscutter.data.def.SceneData; +import emu.grasscutter.tools.Tools; +import emu.grasscutter.utils.FileUtils; +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; + +final class HandbookRequestHandler implements DocumentationHandler { + + private final String template; + private Map map; + + + public HandbookRequestHandler() { + ResourceLoader.loadResources(); + final File templateFile = new File(Utils.toFilePath(DATA("documentation/handbook.html"))); + if (templateFile.exists()) { + template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8); + } else { + Grasscutter.getLogger().warn("File does not exist: " + templateFile); + template = null; + } + + final String textMapFile = "TextMap/TextMap" + Tools.getLanguageOption() + ".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<>(); + } + } + + @Override + public void handle(Request request, Response response) { + if (template == null) { + response.status(500); + return; + } + + final CommandMap cmdMap = new CommandMap(true); + final Int2ObjectMap avatarMap = GameData.getAvatarDataMap(); + final Int2ObjectMap itemMap = GameData.getItemDataMap(); + final Int2ObjectMap sceneMap = GameData.getSceneDataMap(); + final Int2ObjectMap monsterMap = GameData.getMonsterDataMap(); + + // 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")) + // Commands table + .replace("{{COMMANDS_TABLE}}", cmdMap.getAnnotationsAsList() + .stream() + .map(cmd -> "" + cmd.label() + "" + + cmd.description() + "") + .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); + } +} diff --git a/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java b/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java new file mode 100644 index 000000000..56a68cac8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/http/documentation/RootRequestHandler.java @@ -0,0 +1,42 @@ +package emu.grasscutter.server.http.documentation; + +import static emu.grasscutter.Configuration.DATA; +import static emu.grasscutter.utils.Language.translate; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.ResourceLoader; +import emu.grasscutter.utils.FileUtils; +import emu.grasscutter.utils.Utils; +import express.http.Request; +import express.http.Response; +import java.io.File; +import java.nio.charset.StandardCharsets; + +final class RootRequestHandler implements DocumentationHandler { + + private final String template; + + public RootRequestHandler() { + ResourceLoader.loadResources(); + final File templateFile = new File(Utils.toFilePath(DATA("documentation/index.html"))); + if (templateFile.exists()) { + template = new String(FileUtils.read(templateFile), StandardCharsets.UTF_8); + } else { + Grasscutter.getLogger().warn("File does not exist: " + templateFile); + template = null; + } + } + + @Override + public void handle(Request request, Response response) { + if (template == null) { + response.status(500); + return; + } + + String content = template.replace("{{TITLE}}", translate("documentation.index.title")) + .replace("{{ITEM_HANDBOOK}}", translate("documentation.index.handbook")) + .replace("{{ITEM_GACHA_MAPPING}}", translate("documentation.index.gacha_mapping")); + response.send(content); + } +} diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json index b45862f94..f48eafeee 100644 --- a/src/main/resources/languages/en-US.json +++ b/src/main/resources/languages/en-US.json @@ -386,5 +386,27 @@ "available_three_stars": "Available 3-star Items", "template_missing": "data/gacha_details.html is missing." } + }, + "documentation": { + "handbook": { + "title": "GM Handbook", + "title_commands": "Commands", + "title_avatars": "Avatars", + "title_items": "Items", + "title_scenes": "Scenes", + "title_monsters": "Monsters", + "header_id": "Id", + "header_command": "Command", + "header_description": "Description", + "header_avatar": "Avatar", + "header_item": "Item", + "header_scene": "Scene", + "header_monster": "Monster" + }, + "index": { + "title": "Documentation", + "handbook": "GM Handbook", + "gacha_mapping": "Gacha mapping JSON" + } } }