Add quest data dumping for the handbook

This commit is contained in:
KingRainbow44 2023-05-16 19:46:18 -04:00
parent a377fe2107
commit 919f533ed7
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
7 changed files with 218 additions and 16 deletions

View File

@ -2,10 +2,12 @@
Use Grasscutter's dumpers to generate the data to put here. Use Grasscutter's dumpers to generate the data to put here.
## Files Required ## Files Required
- `mainquests.csv'
- `commands.json` - `commands.json`
- `entities.csv` - `entities.csv`
- `avatars.csv` - `avatars.csv`
- `scenes.csv` - `scenes.csv`
- `quests.csv`
- `items.csv` - `items.csv`
# Item Icon Notes # Item Icon Notes

View File

@ -1,17 +1,21 @@
import mainQuests from "@data/mainquests.csv";
import commands from "@data/commands.json"; import commands from "@data/commands.json";
import entities from "@data/entities.csv"; import entities from "@data/entities.csv";
import avatars from "@data/avatars.csv"; import avatars from "@data/avatars.csv";
import scenes from "@data/scenes.csv"; import scenes from "@data/scenes.csv";
import quests from "@data/quests.csv";
import items from "@data/items.csv"; import items from "@data/items.csv";
import { Quality, ItemType, ItemCategory, SceneType } from "@backend/types"; 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"; import { inRange } from "@app/utils";
type AvatarDump = { [key: number]: Avatar }; type AvatarDump = { [key: number]: Avatar };
type CommandDump = { [key: string]: Command }; type CommandDump = { [key: string]: Command };
type TaggedItems = { [key: number]: Item[] }; type TaggedItems = { [key: number]: Item[] };
type QuestDump = { [key: number]: Quest };
type MainQuestDump = { [key: number]: MainQuest };
/** /**
* @see {@file src/handbook/data/README.md} * @see {@file src/handbook/data/README.md}
@ -27,6 +31,8 @@ export const sortedItems: TaggedItems = {
[ItemCategory.Miscellaneous]: [] [ItemCategory.Miscellaneous]: []
}; };
export let allMainQuests: MainQuestDump = {};
/** /**
* Setup function for this file. * Setup function for this file.
* Sorts all items into their respective categories. * Sorts all items into their respective categories.
@ -57,6 +63,8 @@ export function setup(): void {
sortedItems[ItemCategory.Avatar].push(item); 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];
}

View File

@ -2,6 +2,11 @@ export type Page = "Home" | "Commands" | "Avatars" | "Items" | "Entities" | "Sce
export type Overlays = "None" | "ServerSettings"; export type Overlays = "None" | "ServerSettings";
export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday"; export type Days = "Sunday" | "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday";
export type MainQuest = {
id: number;
title: string;
};
export type Command = { export type Command = {
name: string[]; name: string[];
description: string; description: string;
@ -36,6 +41,12 @@ export type Entity = {
internal: string; internal: string;
}; };
export type Quest = {
id: number;
description: string;
mainId: number;
};
// Exported from Project Amber. // Exported from Project Amber.
export type ItemInfo = { export type ItemInfo = {
response: number | 200 | 404; response: number | 200 | 404;

View File

@ -4,6 +4,6 @@
overflow-x: scroll; overflow-x: scroll;
p { p {
color: black; color: white;
} }
} }

View File

@ -1,6 +1,14 @@
import React from "react"; 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"; import "@css/views/PlainText.scss";
@ -13,7 +21,7 @@ class PlainText extends React.PureComponent {
return ( return (
<> <>
{listCommands().map((command) => ( {listCommands().map((command) => (
<p>{`${command.name[0]} : ${command.description}`}</p> <p key={command.name[0]}>{`${command.name[0]} : ${command.description}`}</p>
))} ))}
</> </>
); );
@ -29,7 +37,7 @@ class PlainText extends React.PureComponent {
{listAvatars() {listAvatars()
.sort((a, b) => a.id - b.id) .sort((a, b) => a.id - b.id)
.map((avatar) => ( .map((avatar) => (
<p>{`${avatar.id} : ${avatar.name}`}</p> <p key={avatar.id}>{`${avatar.id} : ${avatar.name}`}</p>
))} ))}
</> </>
); );
@ -45,7 +53,7 @@ class PlainText extends React.PureComponent {
{getItems() {getItems()
.sort((a, b) => a.id - b.id) .sort((a, b) => a.id - b.id)
.map((item) => ( .map((item) => (
<p>{`${item.id} : ${item.name}`}</p> <p key={item.id}>{`${item.id} : ${item.name}`}</p>
))} ))}
</> </>
); );
@ -61,7 +69,7 @@ class PlainText extends React.PureComponent {
{getEntities() {getEntities()
.sort((a, b) => a.id - b.id) .sort((a, b) => a.id - b.id)
.map((entity) => ( .map((entity) => (
<p>{`${entity.id} : ${entity.name}`}</p> <p key={entity.id}>{`${entity.id} : ${entity.name}`}</p>
))} ))}
</> </>
); );
@ -77,7 +85,23 @@ class PlainText extends React.PureComponent {
{getScenes() {getScenes()
.sort((a, b) => a.id - b.id) .sort((a, b) => a.id - b.id)
.map((scene) => ( .map((scene) => (
<p>{`${scene.id} : ${scene.identifier} [${scene.type}]`}</p> <p key={scene.id}>{`${scene.id} : ${scene.identifier} [${scene.type}]`}</p>
))}
</>
);
}
/**
* Creates a paragraph of quests.
* @private
*/
private getQuests(): React.ReactNode {
return (
<>
{listQuests()
.sort((a, b) => a.id - b.id)
.map((quest) => (
<p key={quest.id}>{`${quest.id} : ${getMainQuestFor(quest)?.title ?? "Unknown"} - ${quest.description}`}</p>
))} ))}
</> </>
); );
@ -129,6 +153,14 @@ class PlainText extends React.PureComponent {
</p> </p>
{this.getScenes()} {this.getScenes()}
<p>
<br />
<br />
// Quests
</p>
{this.getQuests()}
</div> </div>
); );
} }

View File

@ -9,6 +9,8 @@ import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Language; import emu.grasscutter.utils.Language;
import lombok.AllArgsConstructor;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -17,7 +19,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
public interface Dumpers { public interface Dumpers {
// See `src/handbook/data/README.md` for attributions. // See `src/handbook/data/README.md` for attributions.
@ -53,6 +54,16 @@ public interface Dumpers {
.collect(Collectors.joining("\n")); .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<Integer, ?> dump, String... headers) {
return String.join(",", headers) + "\n" + Dumpers.miniEncode(dump);
}
/** /**
* Dumps all commands to a JSON file. * 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. * @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. * @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() { static void dumpScenes() {
// Reload resources. // Reload resources.
ResourceLoader.loadAll(); 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. * @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<Integer, QuestInfo>();
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<Integer, MainQuestInfo>();
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 @AllArgsConstructor
class CommandInfo { class CommandInfo {
public List<String> name; public List<String> 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 { enum Quality {
LEGENDARY, LEGENDARY,
EPIC, EPIC,

View File

@ -1,17 +1,18 @@
package emu.grasscutter.utils; package emu.grasscutter.utils;
import static emu.grasscutter.config.Configuration.*;
import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.Logger;
import emu.grasscutter.BuildConfig; import emu.grasscutter.BuildConfig;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.PacketOpcodesUtils; import emu.grasscutter.net.packet.PacketOpcodesUtils;
import emu.grasscutter.tools.Dumpers; import emu.grasscutter.tools.Dumpers;
import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import org.slf4j.LoggerFactory;
import static emu.grasscutter.config.Configuration.*;
/** A parser for start-up arguments. */ /** A parser for start-up arguments. */
public final class StartupArguments { public final class StartupArguments {
@ -167,6 +168,7 @@ public final class StartupArguments {
case "items" -> Dumpers.dumpItems(language); case "items" -> Dumpers.dumpItems(language);
case "scenes" -> Dumpers.dumpScenes(); case "scenes" -> Dumpers.dumpScenes();
case "entities" -> Dumpers.dumpEntities(language); case "entities" -> Dumpers.dumpEntities(language);
case "quests" -> Dumpers.dumpQuests(language);
} }
Grasscutter.getLogger().info("Finished dumping."); Grasscutter.getLogger().info("Finished dumping.");