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.");