make quest categories configurable

This commit is contained in:
Martin Michelsen
2023-06-08 20:43:46 -07:00
parent d6eee92645
commit 25b6c594bd
114 changed files with 257 additions and 251 deletions
+4 -2
View File
@@ -133,14 +133,16 @@ To use newserv in other ways (e.g. for translating data), see the end of this do
newserv automatically finds quests in the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's "save files" option, just put them in that directory and name them appropriately.
Standard quest files should be named like `q###-CATEGORY-VERSION.EXT`, battle quests should be named like `b###-VERSION.EXT`, challenge quests should be named like `c###-VERSION.EXT`, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
Standard quest files should be named like `q###-CATEGORY-VERSION.EXT`, battle quests should be named like `b###-VERSION.EXT`, challenge quests should be named like `c###-VERSION.EXT` for Episode 1 or `d###-VERSION.EXT` for Episode 2, and Episode 3 download quests should be named like `e###-gc3.EXT`. The fields in each filename are:
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gov = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `CATEGORY`: ret = Retrieval, ext = Extermination, evt = Events, shp = Shops, vr = VR, twr = Tower, gv1/gv2/gv4 = Government (BB only), dl = Download (these don't appear during online play), 1p = Solo (BB only)
- `VERSION`: d1 = Dreamcast v1, dc = Dreamcast v2, pc = PC, gc = GameCube Episodes 1 & 2, gc3 = Episode 3, bb = Blue Burst
- `EXT`: file extension (see table below)
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-ret-gc.bin` and `q058-ret-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, and it puts them in the Retrieval category because the filenames contain `-ret`.
The type identifiers (`b`, `c`, `d`, `e`, or `q`) and categories are configurable. See QuestCategories in config.example.json for more information on how to make new categories or edit the existing categories.
There are multiple PSO quest formats out there; newserv supports all of them. It can also decode any known format to standard .bin/.dat format. Specifically:
| Format | Extension | Supported | Decode action |
+90 -112
View File
@@ -20,6 +20,47 @@
using namespace std;
QuestCategoryIndex::Category::Category(uint32_t category_id, std::shared_ptr<const JSONObject> json)
: category_id(category_id) {
const auto& l = json->as_list();
this->flags = l.at(0)->as_int();
this->type = l.at(1)->as_string().at(0);
this->short_token = l.at(2)->as_string();
this->name = decode_sjis(l.at(3)->as_string());
this->description = decode_sjis(l.at(4)->as_string());
}
bool QuestCategoryIndex::Category::matches_flags(uint8_t request) const {
// If the request is for v1 or v2 (hence it has the HIDE_ON_PRE_V3 flag set)
// and the category also has that flag set, it never matches
if (request & this->flags & Flag::HIDE_ON_PRE_V3) {
return false;
}
return request & this->flags;
}
QuestCategoryIndex::QuestCategoryIndex(std::shared_ptr<const JSONObject> json) {
uint32_t next_category_id = 1;
for (const auto& it : json->as_list()) {
this->categories.emplace_back(next_category_id++, it);
}
}
const QuestCategoryIndex::Category& QuestCategoryIndex::find(char type, const std::string& short_token) const {
// Technically we should index these and do a map lookup, but there will
// probably always only be a small constant number of them
for (const auto& it : this->categories) {
if (it.type == type && it.short_token == short_token) {
return it;
}
}
throw out_of_range(string_printf("no category with type %c and short_token %s", type, short_token.c_str()));
}
const QuestCategoryIndex::Category& QuestCategoryIndex::at(uint32_t category_id) const {
return this->categories.at(category_id - 1);
}
// GCI decoding logic
template <bool IsBigEndian>
@@ -189,47 +230,6 @@ struct PSODownloadQuestHeader {
le_uint32_t encryption_seed;
} __attribute__((packed));
bool category_is_mode(QuestCategory category) {
return (category == QuestCategory::BATTLE) ||
(category == QuestCategory::CHALLENGE) ||
(category == QuestCategory::EPISODE_3);
}
const char* name_for_category(QuestCategory category) {
switch (category) {
case QuestCategory::RETRIEVAL:
return "Retrieval";
case QuestCategory::EXTERMINATION:
return "Extermination";
case QuestCategory::EVENT:
return "Event";
case QuestCategory::SHOP:
return "Shop";
case QuestCategory::VR:
return "VR";
case QuestCategory::TOWER:
return "Tower";
case QuestCategory::GOVERNMENT_EPISODE_1:
return "GovernmentEp1";
case QuestCategory::GOVERNMENT_EPISODE_2:
return "GovernmentEp2";
case QuestCategory::GOVERNMENT_EPISODE_4:
return "GovernmentEp4";
case QuestCategory::DOWNLOAD:
return "Download";
case QuestCategory::BATTLE:
return "Battle";
case QuestCategory::CHALLENGE:
return "Challenge";
case QuestCategory::SOLO:
return "Solo";
case QuestCategory::EPISODE_3:
return "Episode3";
default:
return "Unknown";
}
}
struct PSOQuestHeaderDC { // Same format for DC v1 and v2, thankfully
uint32_t start_offset;
uint32_t unknown_offset1;
@@ -288,10 +288,10 @@ struct PSOQuestHeaderBB {
ptext<char16_t, 0x120> long_description;
} __attribute__((packed));
Quest::Quest(const string& bin_filename)
Quest::Quest(const string& bin_filename, shared_ptr<const QuestCategoryIndex> category_index)
: internal_id(-1),
menu_item_id(0),
category(QuestCategory::UNKNOWN),
category_id(0),
episode(Episode::NONE),
is_dcv1(false),
joinable(false),
@@ -345,49 +345,23 @@ Quest::Quest(const string& bin_filename)
throw invalid_argument("empty filename");
}
if (basename[0] == 'b') {
this->category = QuestCategory::BATTLE;
} else if (basename[0] == 'c') {
this->category = QuestCategory::CHALLENGE;
} else if (basename[0] == 'e') {
this->category = QuestCategory::EPISODE_3;
} else if (basename[0] != 'q') {
throw invalid_argument("filename does not indicate mode");
}
if (this->category != QuestCategory::EPISODE_3 && this->has_mnm_extension) {
throw invalid_argument("non-Ep3 quest has .mnm extension");
}
// If the quest category is still unknown, expect 3 tokens (one of them will
// tell us the category)
vector<string> tokens = split(basename, '-');
if (tokens.size() != (2 + (this->category == QuestCategory::UNKNOWN))) {
string category_token;
if (tokens.size() == 3) {
category_token = std::move(tokens[1]);
tokens.erase(tokens.begin() + 1);
} else if (tokens.size() != 2) {
throw invalid_argument("incorrect filename format");
}
auto& category = category_index->find(basename[0], category_token);
this->category_id = category.category_id;
// Parse the number out of the first token
this->internal_id = strtoull(tokens[0].c_str() + 1, nullptr, 10);
// Get the category from the second token if needed
if (this->category == QuestCategory::UNKNOWN) {
static const unordered_map<string, QuestCategory> name_to_category({
{"ret", QuestCategory::RETRIEVAL},
{"ext", QuestCategory::EXTERMINATION},
{"evt", QuestCategory::EVENT},
{"shp", QuestCategory::SHOP},
{"vr", QuestCategory::VR},
{"twr", QuestCategory::TOWER},
// Note: This will be overwritten later for Episode 2 & 4 quests - we
// haven't parsed the episode number from the quest script yet
{"gov", QuestCategory::GOVERNMENT_EPISODE_1},
{"dl", QuestCategory::DOWNLOAD},
{"1p", QuestCategory::SOLO},
});
this->category = name_to_category.at(tokens[1]);
tokens.erase(tokens.begin() + 1);
}
// Get the version from the second (or previously third) token
static const unordered_map<string, GameVersion> name_to_version({
{"d1", GameVersion::DC},
{"dc", GameVersion::DC},
@@ -439,7 +413,7 @@ Quest::Quest(const string& bin_filename)
case GameVersion::XB:
case GameVersion::GC: {
if (this->category == QuestCategory::EPISODE_3) {
if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) {
if (bin_decompressed.size() != sizeof(Episode3::MapDefinition)) {
throw invalid_argument("file is incorrect size");
}
@@ -485,21 +459,16 @@ Quest::Quest(const string& bin_filename)
this->name = header->name;
this->short_description = header->short_description;
this->long_description = header->long_description;
if (this->category == QuestCategory::GOVERNMENT_EPISODE_1) {
if (this->episode == Episode::EP2) {
this->category = QuestCategory::GOVERNMENT_EPISODE_2;
} else if (this->episode == Episode::EP4) {
this->category = QuestCategory::GOVERNMENT_EPISODE_4;
} else if (this->episode != Episode::EP1) {
throw invalid_argument("government quest has invalid episode number");
}
}
break;
}
default:
throw logic_error("invalid quest game version");
}
if (this->has_mnm_extension && this->episode != Episode::EP3) {
throw runtime_error("non-Episode 3 quest has .mnm extension");
}
}
static string basename_for_filename(const string& filename) {
@@ -511,7 +480,7 @@ static string basename_for_filename(const string& filename) {
}
string Quest::bin_filename() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
return string_printf("m%06" PRId64 "p_e.bin", this->internal_id);
} else {
return basename_for_filename(this->file_basename + ".bin");
@@ -519,7 +488,7 @@ string Quest::bin_filename() const {
}
string Quest::dat_filename() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
} else {
return basename_for_filename(this->file_basename + ".dat");
@@ -563,7 +532,7 @@ shared_ptr<const string> Quest::bin_contents() const {
}
shared_ptr<const string> Quest::dat_contents() const {
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests do not have .dat files");
}
if (!this->dat_contents_ptr) {
@@ -916,12 +885,15 @@ void add_write_file_commands(
}
string Quest::export_qst(GameVersion version) const {
if (this->category == QuestCategory::EPISODE_3) {
throw runtime_error("Episode 3 quests cannot be encoded in QST format");
bool is_ep3 = this->episode == Episode::EP3;
if (is_ep3 && !this->is_dlq_encoded) {
throw runtime_error("Episode 3 quests can only be encoded in download QST format");
}
StringWriter w;
// Some tools expect both open file commands at the beginning, hence this
// unfortunate abstraction-breaking.
switch (version) {
case GameVersion::DC:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_DC_44_A6>(w, *this, true);
@@ -942,11 +914,15 @@ string Quest::export_qst(GameVersion version) const {
case GameVersion::GC:
case GameVersion::XB:
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, true);
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, false);
if (!is_ep3) {
add_open_file_command<PSOCommandHeaderDCV3, S_OpenFile_PC_V3_44_A6>(w, *this, false);
}
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded);
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
if (!is_ep3) {
add_write_file_commands<PSOCommandHeaderDCV3>(
w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded);
}
break;
case GameVersion::BB:
add_open_file_command<PSOCommandHeaderBB, S_OpenFile_BB_44_A6>(w, *this, true);
@@ -963,12 +939,14 @@ string Quest::export_qst(GameVersion version) const {
return std::move(w.str());
}
QuestIndex::QuestIndex(const string& directory) : directory(directory) {
auto filename_set = list_directory(this->directory);
vector<string> filenames(filename_set.begin(), filename_set.end());
sort(filenames.begin(), filenames.end());
QuestIndex::QuestIndex(
const string& directory,
std::shared_ptr<const QuestCategoryIndex> category_index)
: directory(directory),
category_index(category_index) {
uint32_t next_menu_item_id = 1;
for (const auto& filename : filenames) {
for (const auto& filename : list_directory_sorted(this->directory)) {
string full_path = this->directory + "/" + filename;
if (ends_with(filename, ".gba")) {
@@ -988,26 +966,26 @@ QuestIndex::QuestIndex(const string& directory) : directory(directory) {
ends_with(filename, ".mnm.dlq") ||
ends_with(filename, ".qst")) {
try {
shared_ptr<Quest> q(new Quest(full_path));
shared_ptr<Quest> q(new Quest(full_path, this->category_index));
q->menu_item_id = next_menu_item_id++;
string ascii_name = encode_sjis(q->name);
if (!this->version_menu_item_id_to_quest.emplace(
make_pair(q->version, q->menu_item_id), q)
.second) {
if (!this->version_menu_item_id_to_quest.emplace(make_pair(q->version, q->menu_item_id), q).second) {
throw logic_error("duplicate quest menu item id");
}
static_game_data_log.info("Indexed quest %s (%s => %s-%" PRId64 " (%" PRIu32 "), %s, %s, joinable=%s, dcv1=%s)",
auto category_name = encode_sjis(this->category_index->at(q->category_id).name);
static_game_data_log.info("Indexed quest %s (%s => %s-%" PRId64 " (%" PRIu32 "), %s, %s (%" PRIu32 "), joinable=%s, dcv1=%s)",
ascii_name.c_str(),
filename.c_str(),
name_for_version(q->version),
q->internal_id,
q->menu_item_id,
name_for_category(q->category),
name_for_episode(q->episode),
category_name.c_str(),
q->category_id,
q->joinable ? "true" : "false",
q->is_dcv1 ? "true" : "false");
} catch (const exception& e) {
static_game_data_log.warning("Failed to parse quest file %s (%s)", filename.c_str(), e.what());
static_game_data_log.warning("Failed to index quest file %s (%s)", filename.c_str(), e.what());
}
}
}
@@ -1023,14 +1001,14 @@ shared_ptr<const string> QuestIndex::get_gba(const string& name) const {
}
vector<shared_ptr<const Quest>> QuestIndex::filter(
GameVersion version, bool is_dcv1, QuestCategory category) const {
GameVersion version, bool is_dcv1, uint32_t category_id) const {
auto it = this->version_menu_item_id_to_quest.lower_bound(make_pair(version, 0));
auto end_it = this->version_menu_item_id_to_quest.upper_bound(make_pair(version, 0xFFFFFFFF));
vector<shared_ptr<const Quest>> ret;
for (; it != end_it; it++) {
shared_ptr<const Quest> q = it->second;
if ((q->is_dcv1 != is_dcv1) || (q->category != category)) {
if ((q->is_dcv1 != is_dcv1) || (q->category_id != category_id)) {
continue;
}
ret.emplace_back(q);
@@ -1076,7 +1054,7 @@ shared_ptr<Quest> Quest::create_download_quest() const {
// This function should not be used for Episode 3 quests (they should be sent
// to the client as-is, without any encryption or other preprocessing)
if (this->category == QuestCategory::EPISODE_3) {
if (this->episode == Episode::EP3) {
throw logic_error("Episode 3 quests cannot be converted to download quests");
}
+36 -23
View File
@@ -10,26 +10,38 @@
#include "StaticGameData.hh"
#include "Version.hh"
enum class QuestCategory {
UNKNOWN = -1,
RETRIEVAL = 0,
EXTERMINATION,
EVENT,
SHOP,
VR,
TOWER,
GOVERNMENT_EPISODE_1,
GOVERNMENT_EPISODE_2,
GOVERNMENT_EPISODE_4,
DOWNLOAD,
BATTLE,
CHALLENGE,
SOLO,
EPISODE_3,
};
struct QuestCategoryIndex {
struct Category {
enum Flag {
NORMAL = 0x01,
BATTLE = 0x02,
CHALLENGE = 0x04,
SOLO = 0x08,
GOVERNMENT = 0x10,
DOWNLOAD = 0x20,
EP3_DOWNLOAD = 0x40,
HIDE_ON_PRE_V3 = 0x80,
};
bool category_is_mode(QuestCategory category);
const char* name_for_category(QuestCategory category);
uint32_t category_id;
uint8_t flags;
char type;
std::string short_token;
std::u16string name;
std::u16string description;
explicit Category(uint32_t category_id, std::shared_ptr<const JSONObject> json);
bool matches_flags(uint8_t request) const;
};
std::vector<Category> categories;
explicit QuestCategoryIndex(std::shared_ptr<const JSONObject> json);
const Category& find(char type, const std::string& short_token) const;
const Category& at(uint32_t category_id) const;
};
class Quest {
public:
@@ -43,7 +55,7 @@ public:
};
int64_t internal_id;
uint32_t menu_item_id;
QuestCategory category;
uint32_t category_id;
Episode episode;
bool is_dcv1;
bool joinable;
@@ -56,7 +68,7 @@ public:
std::u16string short_description;
std::u16string long_description;
Quest(const std::string& file_basename);
Quest(const std::string& file_basename, std::shared_ptr<const QuestCategoryIndex> category_index);
Quest(const Quest&) = default;
Quest(Quest&&) = default;
Quest& operator=(const Quest&) = default;
@@ -91,6 +103,7 @@ private:
struct QuestIndex {
std::string directory;
std::shared_ptr<const QuestCategoryIndex> category_index;
std::map<std::pair<GameVersion, uint64_t>, std::shared_ptr<Quest>> version_menu_item_id_to_quest;
@@ -98,10 +111,10 @@ struct QuestIndex {
std::map<std::string, std::shared_ptr<std::string>> gba_file_contents;
QuestIndex(const std::string& directory);
QuestIndex(const std::string& directory, std::shared_ptr<const QuestCategoryIndex> category_index);
std::shared_ptr<const Quest> get(GameVersion version, uint32_t id) const;
std::shared_ptr<const std::string> get_gba(const std::string& name) const;
std::vector<std::shared_ptr<const Quest>> filter(GameVersion version,
bool is_dcv1, QuestCategory category) const;
bool is_dcv1, uint32_t category_id) const;
};
+26 -51
View File
@@ -30,43 +30,6 @@ const char* QUEST_BARRIER_DISCONNECT_HOOK_NAME = "quest_barrier";
const char* CARD_AUCTION_DISCONNECT_HOOK_NAME = "card_auction";
const char* ADD_NEXT_CLIENT_DISCONNECT_HOOK_NAME = "add_next_game_client";
vector<MenuItem> quest_categories_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
});
vector<MenuItem> quest_battle_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::BATTLE), u"Battle", u"$E$C6Battle mode rule\nsets", 0),
});
vector<MenuItem> quest_challenge_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::CHALLENGE), u"Challenge", u"$E$C6Challenge mode\nquests", 0),
});
vector<MenuItem> quest_solo_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::SOLO), u"Solo Quests", u"$E$C6Quests that require\na single player", 0),
});
vector<MenuItem> quest_government_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_1), u"Hero in Red", u"$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_2), u"The Military's Hero", u"$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::GOVERNMENT_EPISODE_4), u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0),
});
vector<MenuItem> quest_download_menu({
MenuItem(static_cast<uint32_t>(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0),
MenuItem(static_cast<uint32_t>(QuestCategory::VR), u"Virtual Reality", u"$E$C6Quests that are\ndone in a simulator", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::TOWER), u"Control Tower", u"$E$C6Quests that take\nplace at the Control\nTower", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC),
MenuItem(static_cast<uint32_t>(QuestCategory::DOWNLOAD), u"Download", u"$E$C6Quests to download\nto your Memory Card", 0),
});
static shared_ptr<const Menu> proxy_options_menu_for_client(
shared_ptr<ServerState> s, shared_ptr<const Client> c) {
shared_ptr<Menu> ret(new Menu(MenuID::PROXY_OPTIONS, u"Proxy options"));
@@ -1714,16 +1677,25 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
case MainMenuItemID::DOWNLOAD_QUESTS:
if (c->flags & Client::Flag::IS_EPISODE_3) {
shared_ptr<Lobby> l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr;
auto quests = s->quest_index->filter(
c->version(), c->flags & Client::Flag::IS_DC_V1, QuestCategory::EPISODE_3);
// Episode 3 has only download quests, not online quests, so this is
// always the download quest menu. (Episode 3 does actually have
// online quests, but they're served via a server data request
// instead of the file download paradigm that other versions use.)
vector<shared_ptr<const Quest>> quests;
for (const auto& category : s->quest_category_index->categories) {
if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) {
quests = s->quest_index->filter(
c->version(), c->flags & Client::Flag::IS_DC_V1, category.category_id);
break;
}
}
send_quest_menu(c, MenuID::QUEST, quests, true);
} else {
send_quest_menu(c, MenuID::QUEST_FILTER, quest_download_menu, true);
uint8_t flags = QuestCategoryIndex::Category::Flag::DOWNLOAD;
if (c->version() == GameVersion::DC || c->version() == GameVersion::PC) {
flags |= QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3;
}
send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags);
}
break;
@@ -1954,9 +1926,8 @@ static void on_10(shared_ptr<ServerState> s, shared_ptr<Client> c,
break;
}
shared_ptr<Lobby> l = c->lobby_id ? s->find_lobby(c->lobby_id) : nullptr;
auto quests = s->quest_index->filter(c->version(),
c->flags & Client::Flag::IS_DC_V1,
static_cast<QuestCategory>(item_id & 0xFF));
auto quests = s->quest_index->filter(
c->version(), c->flags & Client::Flag::IS_DC_V1, item_id);
// Hack: Assume the menu to be sent is the download quest menu if the
// client is not in any lobby
@@ -2330,29 +2301,33 @@ static void on_A2(shared_ptr<ServerState> s, shared_ptr<Client> c,
send_lobby_message_box(c, u"$C6Episode 3 does not\nprovide online quests\nvia this interface.");
} else {
vector<MenuItem>* menu = nullptr;
uint8_t flags = (c->version() == GameVersion::DC || c->version() == GameVersion::PC)
? QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3
: 0;
if ((c->version() == GameVersion::BB) && flag) {
menu = &quest_government_menu;
flags |= QuestCategoryIndex::Category::Flag::GOVERNMENT;
} else {
switch (l->mode) {
case GameMode::NORMAL:
menu = &quest_categories_menu;
flags |= QuestCategoryIndex::Category::Flag::NORMAL;
break;
case GameMode::BATTLE:
menu = &quest_battle_menu;
flags |= QuestCategoryIndex::Category::Flag::BATTLE;
break;
case GameMode::CHALLENGE:
menu = &quest_challenge_menu;
flags |= QuestCategoryIndex::Category::Flag::CHALLENGE;
break;
case GameMode::SOLO:
menu = &quest_solo_menu;
flags |= QuestCategoryIndex::Category::Flag::SOLO;
break;
default:
throw logic_error("invalid game mode");
}
}
send_quest_menu(c, MenuID::QUEST_FILTER, *menu, false);
send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags);
}
}
+15 -11
View File
@@ -1254,15 +1254,19 @@ template <typename EntryT>
void send_quest_menu_t(
shared_ptr<Client> c,
uint32_t menu_id,
const vector<MenuItem>& items,
bool is_download_menu) {
shared_ptr<const QuestCategoryIndex> category_index,
uint8_t flags) {
bool is_download_menu = flags & (QuestCategoryIndex::Category::Flag::DOWNLOAD | QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD);
vector<EntryT> entries;
for (const auto& item : items) {
for (const auto& category : category_index->categories) {
if (!category.matches_flags(flags)) {
continue;
}
auto& e = entries.emplace_back();
e.menu_id = menu_id;
e.item_id = item.item_id;
e.name = item.name;
e.short_description = item.description;
e.item_id = category.category_id;
e.name = category.name;
e.short_description = category.description;
add_color_inplace(e.short_description);
}
send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries);
@@ -1290,20 +1294,20 @@ void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
}
void send_quest_menu(shared_ptr<Client> c, uint32_t menu_id,
const vector<MenuItem>& items, bool is_download_menu) {
shared_ptr<const QuestCategoryIndex> category_index, uint8_t flags) {
switch (c->version()) {
case GameVersion::PC:
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_PC_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::DC:
case GameVersion::GC:
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_DC_GC_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::XB:
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_XB_A2_A4>(c, menu_id, category_index, flags);
break;
case GameVersion::BB:
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, items, is_download_menu);
send_quest_menu_t<S_QuestMenuEntry_BB_A2_A4>(c, menu_id, category_index, flags);
break;
default:
throw logic_error("unimplemented versioned command");
+1 -1
View File
@@ -258,7 +258,7 @@ void send_game_menu(
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
const std::vector<std::shared_ptr<const Quest>>& quests, bool is_download_menu);
void send_quest_menu(std::shared_ptr<Client> c, uint32_t menu_id,
const std::vector<MenuItem>& items, bool is_download_menu);
std::shared_ptr<const QuestCategoryIndex> category_index, uint8_t flags);
void send_lobby_list(std::shared_ptr<Client> c, std::shared_ptr<ServerState> s);
void send_join_lobby(std::shared_ptr<Client> c, std::shared_ptr<Lobby> l);
+8 -1
View File
@@ -751,6 +751,13 @@ void ServerState::parse_config(shared_ptr<const JSONObject> config_json) {
this->ep3_menu_song = d.at("Episode3MenuSong")->as_int();
} catch (const out_of_range&) {
}
try {
this->quest_category_index.reset(new QuestCategoryIndex(d.at("QuestCategories")));
} catch (const exception& e) {
throw runtime_error(string_printf(
"QuestCategories is missing or invalid in config.json (%s) - see config.example.json for an example", e.what()));
}
}
void ServerState::load_licenses() {
@@ -857,7 +864,7 @@ void ServerState::load_ep3_data() {
void ServerState::load_quest_index() {
config_log.info("Collecting quest metadata");
this->quest_index.reset(new QuestIndex("system/quests"));
this->quest_index.reset(new QuestIndex("system/quests", this->quest_category_index));
}
void ServerState::compile_functions() {
+1
View File
@@ -65,6 +65,7 @@ struct ServerState {
std::shared_ptr<const PatchFileIndex> bb_patch_file_index;
std::shared_ptr<const DOLFileIndex> dol_file_index;
std::shared_ptr<const Episode3::DataIndex> ep3_data_index;
std::shared_ptr<const QuestCategoryIndex> quest_category_index;
std::shared_ptr<const QuestIndex> quest_index;
std::shared_ptr<const LevelTable> level_table;
std::shared_ptr<const BattleParamsIndex> battle_params;
+40
View File
@@ -303,6 +303,46 @@
"Megid": [700, 6],
},
// Quest category configuration. See README.md for information on how quest
// files should be named. This list specifies the quest category names and
// descriptions. (We don't use a map here because the category order
// specified here is the order that appears in the quest menu.)
"QuestCategories": [
// Each entry is [type, token, flags, category_name, description].
// These fields are:
// flags: a bit field containing the following:
// 0x01 - appears in normal mode
// 0x02 - appears in battle mode
// 0x04 - appears in challenge mode
// 0x08 - appears in solo mode (BB)
// 0x10 - appears at government counter (BB)
// 0x20 - appears in download quest menu
// 0x40 - appears in Episode 3 download quest menu
// 0x80 - hidden on pre-V3 versions (DC, PC)
// type: the character that newserv expects at the beginning of the quest
// filename, generally one of b, c, e, or q.
// short_token: the token newserv expects to see in quest filenames after
// the quest number.
// category_name: what appears in the quest menu on the client.
// description: what appears in the category description window (may
// contain color escape codes like $C6).
[0x21, "q", "ret", "Retrieval", "$E$C6Quests that involve\nretrieving an object"],
[0x21, "q", "ext", "Extermination", "$E$C6Quests that involve\ndestroying all\nmonsters"],
[0x21, "q", "evt", "Events", "$E$C6Quests that are part\nof an event"],
[0x21, "q", "shp", "Shops", "$E$C6Quests that contain\nshops"],
[0xA1, "q", "vr", "Virtual Reality", "$E$C6Quests that are\ndone in a simulator"],
[0xA1, "q", "twr", "Control Tower", "$E$C6Quests that take\nplace at the Control\nTower"],
[0x02, "b", "", "Battle", "$E$C6Battle mode rule\nsets"],
[0x04, "c", "", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"],
[0x84, "d", "", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],
[0x08, "q", "1p", "Solo", "$E$C6Quests that require\na single player"],
[0x10, "q", "gv1", "Hero in Red", "$E$CG-Red Ring Rico-\n$C6Quests that follow\nthe Episode 1\nstoryline"],
[0x10, "q", "gv2", "The Military's Hero", "$E$CG-Heathcliff Flowen-\n$C6Quests that follow\nthe Episode 2\nstoryline"],
[0x10, "q", "gv4", "The Meteor Impact Incident", "$E$C6Quests that follow\nthe Episode 4\nstoryline"],
[0x20, "q", "dl", "Download", "$E$C6Quests to download\nto your Memory Card"],
[0x40, "e", "", "Download", "$E$C6Quests to download\nto your Memory Card"],
],
// Whether to enable patches on Episode 3 USA. This functionality depends on
// exploiting a bug in Episode 3, and while it seems to work reliably on
// Dolphin, it hasn't been tested on a real GameCube. So, newserv doesn't

Some files were not shown because too many files have changed in this diff Show More