From 919f533ed72b1c648f9bfdb4e01ec450f365e0b3 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Tue, 16 May 2023 19:46:18 -0400 Subject: [PATCH] Add quest data dumping for the handbook --- src/handbook/data/README.md | 2 + src/handbook/src/backend/data.ts | 63 ++++++++++- src/handbook/src/backend/types.ts | 11 ++ src/handbook/src/css/views/PlainText.scss | 2 +- src/handbook/src/ui/views/PlainText.tsx | 44 +++++++- .../java/emu/grasscutter/tools/Dumpers.java | 104 +++++++++++++++++- .../grasscutter/utils/StartupArguments.java | 8 +- 7 files changed, 218 insertions(+), 16 deletions(-) diff --git a/src/handbook/data/README.md b/src/handbook/data/README.md index cb7adf9eb..c39e19b8d 100644 --- a/src/handbook/data/README.md +++ b/src/handbook/data/README.md @@ -2,10 +2,12 @@ Use Grasscutter's dumpers to generate the data to put here. ## Files Required +- `mainquests.csv' - `commands.json` - `entities.csv` - `avatars.csv` - `scenes.csv` +- `quests.csv` - `items.csv` # Item Icon Notes diff --git a/src/handbook/src/backend/data.ts b/src/handbook/src/backend/data.ts index 678646440..f8a1fc699 100644 --- a/src/handbook/src/backend/data.ts +++ b/src/handbook/src/backend/data.ts @@ -1,17 +1,21 @@ +import mainQuests from "@data/mainquests.csv"; import commands from "@data/commands.json"; import entities from "@data/entities.csv"; import avatars from "@data/avatars.csv"; import scenes from "@data/scenes.csv"; +import quests from "@data/quests.csv"; import items from "@data/items.csv"; import { Quality, ItemType, ItemCategory, SceneType } from "@backend/types"; -import type { Command, Avatar, Item, Scene, Entity } from "@backend/types"; +import type { MainQuest, Command, Avatar, Item, Scene, Entity, Quest } from "@backend/types"; import { inRange } from "@app/utils"; type AvatarDump = { [key: number]: Avatar }; type CommandDump = { [key: string]: Command }; type TaggedItems = { [key: number]: Item[] }; +type QuestDump = { [key: number]: Quest }; +type MainQuestDump = { [key: number]: MainQuest }; /** * @see {@file src/handbook/data/README.md} @@ -27,6 +31,8 @@ export const sortedItems: TaggedItems = { [ItemCategory.Miscellaneous]: [] }; +export let allMainQuests: MainQuestDump = {}; + /** * Setup function for this file. * Sorts all items into their respective categories. @@ -57,6 +63,8 @@ export function setup(): void { sortedItems[ItemCategory.Avatar].push(item); } }); + + allMainQuests = getMainQuests(); } /** @@ -148,3 +156,56 @@ export function getItems(): Item[] { }; }); } + +/** + * Fetches and casts all quests in the file. + */ +export function getQuests(): QuestDump { + const map: QuestDump = {}; + quests.forEach((quest: Quest) => { + quest.description = quest.description + .replaceAll("\\", ","); + map[quest.id] = quest; + }); + + return map; +} + +/** + * Fetches and lists all the quests in the file. + */ +export function listQuests(): Quest[] { + return Object.values(getQuests()) + .sort((a, b) => a.id - b.id); +} + +/** + * Fetches and casts all quests in the file. + */ +export function getMainQuests(): MainQuestDump { + const map: MainQuestDump = {}; + mainQuests.forEach((quest: MainQuest) => { + quest.title = quest.title + .replaceAll("\\", ","); + map[quest.id] = quest; + }); + + return map; +} + +/** + * Fetches and lists all the quests in the file. + */ +export function listMainQuests(): MainQuestDump[] { + return Object.values(allMainQuests) + .sort((a, b) => a.id - b.id); +} + +/** + * Fetches a quest by its ID. + * + * @param quest The quest ID. + */ +export function getMainQuestFor(quest: Quest): MainQuest { + return allMainQuests[quest.mainId]; +} diff --git a/src/handbook/src/backend/types.ts b/src/handbook/src/backend/types.ts index 0fc6f8e6b..7214b2805 100644 --- a/src/handbook/src/backend/types.ts +++ b/src/handbook/src/backend/types.ts @@ -2,6 +2,11 @@ export type Page = "Home" | "Commands" | "Avatars" | "Items" | "Entities" | "Sce export type Overlays = "None" | "ServerSettings"; export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday"; +export type MainQuest = { + id: number; + title: string; +}; + export type Command = { name: string[]; description: string; @@ -36,6 +41,12 @@ export type Entity = { internal: string; }; +export type Quest = { + id: number; + description: string; + mainId: number; +}; + // Exported from Project Amber. export type ItemInfo = { response: number | 200 | 404; diff --git a/src/handbook/src/css/views/PlainText.scss b/src/handbook/src/css/views/PlainText.scss index bb0fef0bf..29b4336a1 100644 --- a/src/handbook/src/css/views/PlainText.scss +++ b/src/handbook/src/css/views/PlainText.scss @@ -4,6 +4,6 @@ overflow-x: scroll; p { - color: black; + color: white; } } diff --git a/src/handbook/src/ui/views/PlainText.tsx b/src/handbook/src/ui/views/PlainText.tsx index b5a1d3a9e..1e34b05c8 100644 --- a/src/handbook/src/ui/views/PlainText.tsx +++ b/src/handbook/src/ui/views/PlainText.tsx @@ -1,6 +1,14 @@ import React from "react"; -import { listCommands, listAvatars, getItems, getEntities, getScenes } from "@backend/data"; +import { + listCommands, + listAvatars, + getItems, + getEntities, + getScenes, + listQuests, + getMainQuestFor +} from "@backend/data"; import "@css/views/PlainText.scss"; @@ -13,7 +21,7 @@ class PlainText extends React.PureComponent { return ( <> {listCommands().map((command) => ( -

{`${command.name[0]} : ${command.description}`}

+

{`${command.name[0]} : ${command.description}`}

))} ); @@ -29,7 +37,7 @@ class PlainText extends React.PureComponent { {listAvatars() .sort((a, b) => a.id - b.id) .map((avatar) => ( -

{`${avatar.id} : ${avatar.name}`}

+

{`${avatar.id} : ${avatar.name}`}

))} ); @@ -45,7 +53,7 @@ class PlainText extends React.PureComponent { {getItems() .sort((a, b) => a.id - b.id) .map((item) => ( -

{`${item.id} : ${item.name}`}

+

{`${item.id} : ${item.name}`}

))} ); @@ -61,7 +69,7 @@ class PlainText extends React.PureComponent { {getEntities() .sort((a, b) => a.id - b.id) .map((entity) => ( -

{`${entity.id} : ${entity.name}`}

+

{`${entity.id} : ${entity.name}`}

))} ); @@ -77,7 +85,23 @@ class PlainText extends React.PureComponent { {getScenes() .sort((a, b) => a.id - b.id) .map((scene) => ( -

{`${scene.id} : ${scene.identifier} [${scene.type}]`}

+

{`${scene.id} : ${scene.identifier} [${scene.type}]`}

+ ))} + + ); + } + + /** + * Creates a paragraph of quests. + * @private + */ + private getQuests(): React.ReactNode { + return ( + <> + {listQuests() + .sort((a, b) => a.id - b.id) + .map((quest) => ( +

{`${quest.id} : ${getMainQuestFor(quest)?.title ?? "Unknown"} - ${quest.description}`}

))} ); @@ -129,6 +153,14 @@ class PlainText extends React.PureComponent {

{this.getScenes()} + +

+
+
+ // Quests +

+ + {this.getQuests()} ); } diff --git a/src/main/java/emu/grasscutter/tools/Dumpers.java b/src/main/java/emu/grasscutter/tools/Dumpers.java index 9ceb86486..9857d4815 100644 --- a/src/main/java/emu/grasscutter/tools/Dumpers.java +++ b/src/main/java/emu/grasscutter/tools/Dumpers.java @@ -9,6 +9,8 @@ import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.props.SceneType; import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.Language; +import lombok.AllArgsConstructor; + import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -17,7 +19,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import lombok.AllArgsConstructor; public interface Dumpers { // See `src/handbook/data/README.md` for attributions. @@ -53,6 +54,16 @@ public interface Dumpers { .collect(Collectors.joining("\n")); } + /** + * Encodes the dump into comma separated values. + * + * @param dump The dump to encode. + * @return The encoded dump. + */ + private static String miniEncode(Map dump, String... headers) { + return String.join(",", headers) + "\n" + Dumpers.miniEncode(dump); + } + /** * Dumps all commands to a JSON file. * @@ -104,7 +115,7 @@ public interface Dumpers { } /** - * Dumps all avatars to a JSON file. + * Dumps all avatars to a CSV file. * * @param locale The language to dump the avatars in. */ @@ -145,7 +156,7 @@ public interface Dumpers { } /** - * Dumps all items to a JSON file. + * Dumps all items to a CSVv file. * * @param locale The language to dump the items in. */ @@ -195,7 +206,7 @@ public interface Dumpers { } } - /** Dumps all scenes to a JSON file. */ + /** Dumps all scenes to a CSV file. */ static void dumpScenes() { // Reload resources. ResourceLoader.loadAll(); @@ -223,7 +234,7 @@ public interface Dumpers { } /** - * Dumps all entities to a JSON file. + * Dumps all entities to a CSV file. * * @param locale The language to dump the entities in. */ @@ -261,6 +272,68 @@ public interface Dumpers { } } + /** + * Dumps all quests to a JSON file. + * + * @param locale The language to dump the quests in. + */ + static void dumpQuests(String locale) { + // Reload resources. + ResourceLoader.loadAll(); + Language.loadTextMaps(); + + // Convert all known quests to a quest map. + var dump = new HashMap(); + GameData.getQuestDataMap().forEach((id, quest) -> { + var langHash = quest.getDescTextMapHash(); + dump.put(id, new QuestInfo( + langHash == 0 ? "Unknown" : + Language.getTextMapKey(langHash).get(locale) + .replaceAll(",", "\\\\"), + quest.getMainId() + )); + }); + + // Convert all known main quests into a quest map. + var mainDump = new HashMap(); + GameData.getMainQuestDataMap().forEach((id, mainQuest) -> { + var langHash = mainQuest.getTitleTextMapHash(); + mainDump.put(id, new MainQuestInfo( + langHash == 0 ? "Unknown" : + Language.getTextMapKey(langHash).get(locale) + .replaceAll(",", "\\\\") + )); + }); + + try { + // Create a file for the dump. + var file = new File("quests.csv"); + if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); + if (!file.exists() && !file.createNewFile()) + throw new RuntimeException("Failed to create file."); + + // Write the dump to the file. + Files.writeString(file.toPath(), Dumpers.miniEncode(dump, + "id", "description", "mainId")); + } catch (IOException ignored) { + throw new RuntimeException("Failed to write to file."); + } + + try { + // Create a file for the dump. + var file = new File("mainquests.csv"); + if (file.exists() && !file.delete()) throw new RuntimeException("Failed to delete file."); + if (!file.exists() && !file.createNewFile()) + throw new RuntimeException("Failed to create file."); + + // Write the dump to the file. + Files.writeString(file.toPath(), Dumpers.miniEncode(mainDump, + "id", "title")); + } catch (IOException ignored) { + throw new RuntimeException("Failed to write to file."); + } + } + @AllArgsConstructor class CommandInfo { public List name; @@ -317,6 +390,27 @@ public interface Dumpers { } } + @AllArgsConstructor + class MainQuestInfo { + public String title; + + @Override + public String toString() { + return this.title; + } + } + + @AllArgsConstructor + class QuestInfo { + public String description; + public int mainQuest; + + @Override + public String toString() { + return this.description + "," + this.mainQuest; + } + } + enum Quality { LEGENDARY, EPIC, diff --git a/src/main/java/emu/grasscutter/utils/StartupArguments.java b/src/main/java/emu/grasscutter/utils/StartupArguments.java index f9fa8a66a..07eac7a35 100644 --- a/src/main/java/emu/grasscutter/utils/StartupArguments.java +++ b/src/main/java/emu/grasscutter/utils/StartupArguments.java @@ -1,17 +1,18 @@ package emu.grasscutter.utils; -import static emu.grasscutter.config.Configuration.*; - import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import emu.grasscutter.BuildConfig; import emu.grasscutter.Grasscutter; import emu.grasscutter.net.packet.PacketOpcodesUtils; import emu.grasscutter.tools.Dumpers; +import org.slf4j.LoggerFactory; + import java.util.HashMap; import java.util.Map; import java.util.function.Function; -import org.slf4j.LoggerFactory; + +import static emu.grasscutter.config.Configuration.*; /** A parser for start-up arguments. */ public final class StartupArguments { @@ -167,6 +168,7 @@ public final class StartupArguments { case "items" -> Dumpers.dumpItems(language); case "scenes" -> Dumpers.dumpScenes(); case "entities" -> Dumpers.dumpEntities(language); + case "quests" -> Dumpers.dumpQuests(language); } Grasscutter.getLogger().info("Finished dumping.");