diff --git a/README.md b/README.md index 2f294818..12fdb52f 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/Quest.cc b/src/Quest.cc index 5943b5bc..f173816a 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -20,6 +20,47 @@ using namespace std; +QuestCategoryIndex::Category::Category(uint32_t category_id, std::shared_ptr 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 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 @@ -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 long_description; } __attribute__((packed)); -Quest::Quest(const string& bin_filename) +Quest::Quest(const string& bin_filename, shared_ptr 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 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 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 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 Quest::bin_contents() const { } shared_ptr 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(w, *this, true); @@ -942,11 +914,15 @@ string Quest::export_qst(GameVersion version) const { case GameVersion::GC: case GameVersion::XB: add_open_file_command(w, *this, true); - add_open_file_command(w, *this, false); + if (!is_ep3) { + add_open_file_command(w, *this, false); + } add_write_file_commands( w, this->file_basename + ".bin", *this->bin_contents(), this->is_dlq_encoded); - add_write_file_commands( - w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + if (!is_ep3) { + add_write_file_commands( + w, this->file_basename + ".dat", *this->dat_contents(), this->is_dlq_encoded); + } break; case GameVersion::BB: add_open_file_command(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 filenames(filename_set.begin(), filename_set.end()); - sort(filenames.begin(), filenames.end()); +QuestIndex::QuestIndex( + const string& directory, + std::shared_ptr 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 q(new Quest(full_path)); + shared_ptr 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 QuestIndex::get_gba(const string& name) const { } vector> 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> ret; for (; it != end_it; it++) { shared_ptr 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::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"); } diff --git a/src/Quest.hh b/src/Quest.hh index 12affdc0..85b46470 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -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 json); + + bool matches_flags(uint8_t request) const; + }; + + std::vector categories; + + explicit QuestCategoryIndex(std::shared_ptr 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 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 category_index; std::map, std::shared_ptr> version_menu_item_id_to_quest; @@ -98,10 +111,10 @@ struct QuestIndex { std::map> gba_file_contents; - QuestIndex(const std::string& directory); + QuestIndex(const std::string& directory, std::shared_ptr category_index); std::shared_ptr get(GameVersion version, uint32_t id) const; std::shared_ptr get_gba(const std::string& name) const; std::vector> filter(GameVersion version, - bool is_dcv1, QuestCategory category) const; + bool is_dcv1, uint32_t category_id) const; }; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index f353582b..c08ad4d7 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -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 quest_categories_menu({ - MenuItem(static_cast(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), - MenuItem(static_cast(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), - MenuItem(static_cast(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0), - MenuItem(static_cast(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0), - MenuItem(static_cast(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(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 quest_battle_menu({ - MenuItem(static_cast(QuestCategory::BATTLE), u"Battle", u"$E$C6Battle mode rule\nsets", 0), -}); - -vector quest_challenge_menu({ - MenuItem(static_cast(QuestCategory::CHALLENGE), u"Challenge", u"$E$C6Challenge mode\nquests", 0), -}); - -vector quest_solo_menu({ - MenuItem(static_cast(QuestCategory::SOLO), u"Solo Quests", u"$E$C6Quests that require\na single player", 0), -}); - -vector quest_government_menu({ - MenuItem(static_cast(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(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(QuestCategory::GOVERNMENT_EPISODE_4), u"The Meteor Impact Incident", u"$E$C6Quests that follow\nthe Episode 4\nstoryline", 0), -}); - -vector quest_download_menu({ - MenuItem(static_cast(QuestCategory::RETRIEVAL), u"Retrieval", u"$E$C6Quests that involve\nretrieving an object", 0), - MenuItem(static_cast(QuestCategory::EXTERMINATION), u"Extermination", u"$E$C6Quests that involve\ndestroying all\nmonsters", 0), - MenuItem(static_cast(QuestCategory::EVENT), u"Events", u"$E$C6Quests that are part\nof an event", 0), - MenuItem(static_cast(QuestCategory::SHOP), u"Shops", u"$E$C6Quests that contain\nshops", 0), - MenuItem(static_cast(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(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(QuestCategory::DOWNLOAD), u"Download", u"$E$C6Quests to download\nto your Memory Card", 0), -}); - static shared_ptr proxy_options_menu_for_client( shared_ptr s, shared_ptr c) { shared_ptr ret(new Menu(MenuID::PROXY_OPTIONS, u"Proxy options")); @@ -1714,16 +1677,25 @@ static void on_10(shared_ptr s, shared_ptr c, case MainMenuItemID::DOWNLOAD_QUESTS: if (c->flags & Client::Flag::IS_EPISODE_3) { - shared_ptr 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> 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 s, shared_ptr c, break; } shared_ptr 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(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 s, shared_ptr c, send_lobby_message_box(c, u"$C6Episode 3 does not\nprovide online quests\nvia this interface."); } else { - vector* 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); } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 6ff5e14b..53b890ad 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1254,15 +1254,19 @@ template void send_quest_menu_t( shared_ptr c, uint32_t menu_id, - const vector& items, - bool is_download_menu) { + shared_ptr category_index, + uint8_t flags) { + bool is_download_menu = flags & (QuestCategoryIndex::Category::Flag::DOWNLOAD | QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD); vector 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 c, uint32_t menu_id, } void send_quest_menu(shared_ptr c, uint32_t menu_id, - const vector& items, bool is_download_menu) { + shared_ptr category_index, uint8_t flags) { switch (c->version()) { case GameVersion::PC: - send_quest_menu_t(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, category_index, flags); break; case GameVersion::DC: case GameVersion::GC: - send_quest_menu_t(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, category_index, flags); break; case GameVersion::XB: - send_quest_menu_t(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, category_index, flags); break; case GameVersion::BB: - send_quest_menu_t(c, menu_id, items, is_download_menu); + send_quest_menu_t(c, menu_id, category_index, flags); break; default: throw logic_error("unimplemented versioned command"); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 087ce501..9f11c9f3 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -258,7 +258,7 @@ void send_game_menu( void send_quest_menu(std::shared_ptr c, uint32_t menu_id, const std::vector>& quests, bool is_download_menu); void send_quest_menu(std::shared_ptr c, uint32_t menu_id, - const std::vector& items, bool is_download_menu); + std::shared_ptr category_index, uint8_t flags); void send_lobby_list(std::shared_ptr c, std::shared_ptr s); void send_join_lobby(std::shared_ptr c, std::shared_ptr l); diff --git a/src/ServerState.cc b/src/ServerState.cc index 0ff12294..4a5e7925 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -751,6 +751,13 @@ void ServerState::parse_config(shared_ptr 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() { diff --git a/src/ServerState.hh b/src/ServerState.hh index 2385ed24..aa3b5ea9 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -65,6 +65,7 @@ struct ServerState { std::shared_ptr bb_patch_file_index; std::shared_ptr dol_file_index; std::shared_ptr ep3_data_index; + std::shared_ptr quest_category_index; std::shared_ptr quest_index; std::shared_ptr level_table; std::shared_ptr battle_params; diff --git a/system/config.example.json b/system/config.example.json index f1cfb965..f5c04644 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -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 diff --git a/system/quests/c201-bb.bin b/system/quests/d201-bb.bin similarity index 100% rename from system/quests/c201-bb.bin rename to system/quests/d201-bb.bin diff --git a/system/quests/c201-bb.dat b/system/quests/d201-bb.dat similarity index 100% rename from system/quests/c201-bb.dat rename to system/quests/d201-bb.dat diff --git a/system/quests/c201-gc.bin b/system/quests/d201-gc.bin similarity index 100% rename from system/quests/c201-gc.bin rename to system/quests/d201-gc.bin diff --git a/system/quests/c201-gc.dat b/system/quests/d201-gc.dat similarity index 100% rename from system/quests/c201-gc.dat rename to system/quests/d201-gc.dat diff --git a/system/quests/c202-bb.bin b/system/quests/d202-bb.bin similarity index 100% rename from system/quests/c202-bb.bin rename to system/quests/d202-bb.bin diff --git a/system/quests/c202-bb.dat b/system/quests/d202-bb.dat similarity index 100% rename from system/quests/c202-bb.dat rename to system/quests/d202-bb.dat diff --git a/system/quests/c202-gc.bin b/system/quests/d202-gc.bin similarity index 100% rename from system/quests/c202-gc.bin rename to system/quests/d202-gc.bin diff --git a/system/quests/c202-gc.dat b/system/quests/d202-gc.dat similarity index 100% rename from system/quests/c202-gc.dat rename to system/quests/d202-gc.dat diff --git a/system/quests/c203-bb.bin b/system/quests/d203-bb.bin similarity index 100% rename from system/quests/c203-bb.bin rename to system/quests/d203-bb.bin diff --git a/system/quests/c203-bb.dat b/system/quests/d203-bb.dat similarity index 100% rename from system/quests/c203-bb.dat rename to system/quests/d203-bb.dat diff --git a/system/quests/c203-gc.bin b/system/quests/d203-gc.bin similarity index 100% rename from system/quests/c203-gc.bin rename to system/quests/d203-gc.bin diff --git a/system/quests/c203-gc.dat b/system/quests/d203-gc.dat similarity index 100% rename from system/quests/c203-gc.dat rename to system/quests/d203-gc.dat diff --git a/system/quests/c204-bb.bin b/system/quests/d204-bb.bin similarity index 100% rename from system/quests/c204-bb.bin rename to system/quests/d204-bb.bin diff --git a/system/quests/c204-bb.dat b/system/quests/d204-bb.dat similarity index 100% rename from system/quests/c204-bb.dat rename to system/quests/d204-bb.dat diff --git a/system/quests/c204-gc.bin b/system/quests/d204-gc.bin similarity index 100% rename from system/quests/c204-gc.bin rename to system/quests/d204-gc.bin diff --git a/system/quests/c204-gc.dat b/system/quests/d204-gc.dat similarity index 100% rename from system/quests/c204-gc.dat rename to system/quests/d204-gc.dat diff --git a/system/quests/c205-bb.bin b/system/quests/d205-bb.bin similarity index 100% rename from system/quests/c205-bb.bin rename to system/quests/d205-bb.bin diff --git a/system/quests/c205-bb.dat b/system/quests/d205-bb.dat similarity index 100% rename from system/quests/c205-bb.dat rename to system/quests/d205-bb.dat diff --git a/system/quests/c205-gc.bin b/system/quests/d205-gc.bin similarity index 100% rename from system/quests/c205-gc.bin rename to system/quests/d205-gc.bin diff --git a/system/quests/c205-gc.dat b/system/quests/d205-gc.dat similarity index 100% rename from system/quests/c205-gc.dat rename to system/quests/d205-gc.dat diff --git a/system/quests/q401-gov-bb.bin b/system/quests/q401-gv1-bb.bin similarity index 100% rename from system/quests/q401-gov-bb.bin rename to system/quests/q401-gv1-bb.bin diff --git a/system/quests/q401-gov-bb.dat b/system/quests/q401-gv1-bb.dat similarity index 100% rename from system/quests/q401-gov-bb.dat rename to system/quests/q401-gv1-bb.dat diff --git a/system/quests/q402-gov-bb.bin b/system/quests/q402-gv1-bb.bin similarity index 100% rename from system/quests/q402-gov-bb.bin rename to system/quests/q402-gv1-bb.bin diff --git a/system/quests/q402-gov-bb.dat b/system/quests/q402-gv1-bb.dat similarity index 100% rename from system/quests/q402-gov-bb.dat rename to system/quests/q402-gv1-bb.dat diff --git a/system/quests/q403-gov-bb.bin b/system/quests/q403-gv1-bb.bin similarity index 100% rename from system/quests/q403-gov-bb.bin rename to system/quests/q403-gv1-bb.bin diff --git a/system/quests/q403-gov-bb.dat b/system/quests/q403-gv1-bb.dat similarity index 100% rename from system/quests/q403-gov-bb.dat rename to system/quests/q403-gv1-bb.dat diff --git a/system/quests/q404-gov-bb.bin b/system/quests/q404-gv1-bb.bin similarity index 100% rename from system/quests/q404-gov-bb.bin rename to system/quests/q404-gv1-bb.bin diff --git a/system/quests/q404-gov-bb.dat b/system/quests/q404-gv1-bb.dat similarity index 100% rename from system/quests/q404-gov-bb.dat rename to system/quests/q404-gv1-bb.dat diff --git a/system/quests/q405-gov-bb.bin b/system/quests/q405-gv1-bb.bin similarity index 100% rename from system/quests/q405-gov-bb.bin rename to system/quests/q405-gv1-bb.bin diff --git a/system/quests/q405-gov-bb.dat b/system/quests/q405-gv1-bb.dat similarity index 100% rename from system/quests/q405-gov-bb.dat rename to system/quests/q405-gv1-bb.dat diff --git a/system/quests/q406-gov-bb.bin b/system/quests/q406-gv1-bb.bin similarity index 100% rename from system/quests/q406-gov-bb.bin rename to system/quests/q406-gv1-bb.bin diff --git a/system/quests/q406-gov-bb.dat b/system/quests/q406-gv1-bb.dat similarity index 100% rename from system/quests/q406-gov-bb.dat rename to system/quests/q406-gv1-bb.dat diff --git a/system/quests/q407-gov-bb.bin b/system/quests/q407-gv1-bb.bin similarity index 100% rename from system/quests/q407-gov-bb.bin rename to system/quests/q407-gv1-bb.bin diff --git a/system/quests/q407-gov-bb.dat b/system/quests/q407-gv1-bb.dat similarity index 100% rename from system/quests/q407-gov-bb.dat rename to system/quests/q407-gv1-bb.dat diff --git a/system/quests/q408-gov-bb.bin b/system/quests/q408-gv1-bb.bin similarity index 100% rename from system/quests/q408-gov-bb.bin rename to system/quests/q408-gv1-bb.bin diff --git a/system/quests/q408-gov-bb.dat b/system/quests/q408-gv1-bb.dat similarity index 100% rename from system/quests/q408-gov-bb.dat rename to system/quests/q408-gv1-bb.dat diff --git a/system/quests/q409-gov-bb.bin b/system/quests/q409-gv1-bb.bin similarity index 100% rename from system/quests/q409-gov-bb.bin rename to system/quests/q409-gv1-bb.bin diff --git a/system/quests/q409-gov-bb.dat b/system/quests/q409-gv1-bb.dat similarity index 100% rename from system/quests/q409-gov-bb.dat rename to system/quests/q409-gv1-bb.dat diff --git a/system/quests/q410-gov-bb.bin b/system/quests/q410-gv1-bb.bin similarity index 100% rename from system/quests/q410-gov-bb.bin rename to system/quests/q410-gv1-bb.bin diff --git a/system/quests/q410-gov-bb.dat b/system/quests/q410-gv1-bb.dat similarity index 100% rename from system/quests/q410-gov-bb.dat rename to system/quests/q410-gv1-bb.dat diff --git a/system/quests/q411-gov-bb.bin b/system/quests/q411-gv1-bb.bin similarity index 100% rename from system/quests/q411-gov-bb.bin rename to system/quests/q411-gv1-bb.bin diff --git a/system/quests/q411-gov-bb.dat b/system/quests/q411-gv1-bb.dat similarity index 100% rename from system/quests/q411-gov-bb.dat rename to system/quests/q411-gv1-bb.dat diff --git a/system/quests/q412-gov-bb.bin b/system/quests/q412-gv1-bb.bin similarity index 100% rename from system/quests/q412-gov-bb.bin rename to system/quests/q412-gv1-bb.bin diff --git a/system/quests/q412-gov-bb.dat b/system/quests/q412-gv1-bb.dat similarity index 100% rename from system/quests/q412-gov-bb.dat rename to system/quests/q412-gv1-bb.dat diff --git a/system/quests/q413-gov-bb.bin b/system/quests/q413-gv1-bb.bin similarity index 100% rename from system/quests/q413-gov-bb.bin rename to system/quests/q413-gv1-bb.bin diff --git a/system/quests/q413-gov-bb.dat b/system/quests/q413-gv1-bb.dat similarity index 100% rename from system/quests/q413-gov-bb.dat rename to system/quests/q413-gv1-bb.dat diff --git a/system/quests/q414-gov-bb.bin b/system/quests/q414-gv1-bb.bin similarity index 100% rename from system/quests/q414-gov-bb.bin rename to system/quests/q414-gv1-bb.bin diff --git a/system/quests/q414-gov-bb.dat b/system/quests/q414-gv1-bb.dat similarity index 100% rename from system/quests/q414-gov-bb.dat rename to system/quests/q414-gv1-bb.dat diff --git a/system/quests/q415-gov-bb.bin b/system/quests/q415-gv1-bb.bin similarity index 100% rename from system/quests/q415-gov-bb.bin rename to system/quests/q415-gv1-bb.bin diff --git a/system/quests/q415-gov-bb.dat b/system/quests/q415-gv1-bb.dat similarity index 100% rename from system/quests/q415-gov-bb.dat rename to system/quests/q415-gv1-bb.dat diff --git a/system/quests/q451-gov-bb.bin b/system/quests/q451-gv2-bb.bin similarity index 100% rename from system/quests/q451-gov-bb.bin rename to system/quests/q451-gv2-bb.bin diff --git a/system/quests/q451-gov-bb.dat b/system/quests/q451-gv2-bb.dat similarity index 100% rename from system/quests/q451-gov-bb.dat rename to system/quests/q451-gv2-bb.dat diff --git a/system/quests/q452-gov-bb.bin b/system/quests/q452-gv2-bb.bin similarity index 100% rename from system/quests/q452-gov-bb.bin rename to system/quests/q452-gv2-bb.bin diff --git a/system/quests/q452-gov-bb.dat b/system/quests/q452-gv2-bb.dat similarity index 100% rename from system/quests/q452-gov-bb.dat rename to system/quests/q452-gv2-bb.dat diff --git a/system/quests/q453-gov-bb.bin b/system/quests/q453-gv2-bb.bin similarity index 100% rename from system/quests/q453-gov-bb.bin rename to system/quests/q453-gv2-bb.bin diff --git a/system/quests/q453-gov-bb.dat b/system/quests/q453-gv2-bb.dat similarity index 100% rename from system/quests/q453-gov-bb.dat rename to system/quests/q453-gv2-bb.dat diff --git a/system/quests/q454-gov-bb.bin b/system/quests/q454-gv2-bb.bin similarity index 100% rename from system/quests/q454-gov-bb.bin rename to system/quests/q454-gv2-bb.bin diff --git a/system/quests/q454-gov-bb.dat b/system/quests/q454-gv2-bb.dat similarity index 100% rename from system/quests/q454-gov-bb.dat rename to system/quests/q454-gv2-bb.dat diff --git a/system/quests/q455-gov-bb.bin b/system/quests/q455-gv2-bb.bin similarity index 100% rename from system/quests/q455-gov-bb.bin rename to system/quests/q455-gv2-bb.bin diff --git a/system/quests/q455-gov-bb.dat b/system/quests/q455-gv2-bb.dat similarity index 100% rename from system/quests/q455-gov-bb.dat rename to system/quests/q455-gv2-bb.dat diff --git a/system/quests/q456-gov-bb.bin b/system/quests/q456-gv2-bb.bin similarity index 100% rename from system/quests/q456-gov-bb.bin rename to system/quests/q456-gv2-bb.bin diff --git a/system/quests/q456-gov-bb.dat b/system/quests/q456-gv2-bb.dat similarity index 100% rename from system/quests/q456-gov-bb.dat rename to system/quests/q456-gv2-bb.dat diff --git a/system/quests/q457-gov-bb.bin b/system/quests/q457-gv2-bb.bin similarity index 100% rename from system/quests/q457-gov-bb.bin rename to system/quests/q457-gv2-bb.bin diff --git a/system/quests/q457-gov-bb.dat b/system/quests/q457-gv2-bb.dat similarity index 100% rename from system/quests/q457-gov-bb.dat rename to system/quests/q457-gv2-bb.dat diff --git a/system/quests/q458-gov-bb.bin b/system/quests/q458-gv2-bb.bin similarity index 100% rename from system/quests/q458-gov-bb.bin rename to system/quests/q458-gv2-bb.bin diff --git a/system/quests/q458-gov-bb.dat b/system/quests/q458-gv2-bb.dat similarity index 100% rename from system/quests/q458-gov-bb.dat rename to system/quests/q458-gv2-bb.dat diff --git a/system/quests/q459-gov-bb.bin b/system/quests/q459-gv2-bb.bin similarity index 100% rename from system/quests/q459-gov-bb.bin rename to system/quests/q459-gv2-bb.bin diff --git a/system/quests/q459-gov-bb.dat b/system/quests/q459-gv2-bb.dat similarity index 100% rename from system/quests/q459-gov-bb.dat rename to system/quests/q459-gv2-bb.dat diff --git a/system/quests/q460-gov-bb.bin b/system/quests/q460-gv2-bb.bin similarity index 100% rename from system/quests/q460-gov-bb.bin rename to system/quests/q460-gv2-bb.bin diff --git a/system/quests/q460-gov-bb.dat b/system/quests/q460-gv2-bb.dat similarity index 100% rename from system/quests/q460-gov-bb.dat rename to system/quests/q460-gv2-bb.dat diff --git a/system/quests/q461-gov-bb.bin b/system/quests/q461-gv2-bb.bin similarity index 100% rename from system/quests/q461-gov-bb.bin rename to system/quests/q461-gv2-bb.bin diff --git a/system/quests/q461-gov-bb.dat b/system/quests/q461-gv2-bb.dat similarity index 100% rename from system/quests/q461-gov-bb.dat rename to system/quests/q461-gv2-bb.dat diff --git a/system/quests/q462-gov-bb.bin b/system/quests/q462-gv2-bb.bin similarity index 100% rename from system/quests/q462-gov-bb.bin rename to system/quests/q462-gv2-bb.bin diff --git a/system/quests/q462-gov-bb.dat b/system/quests/q462-gv2-bb.dat similarity index 100% rename from system/quests/q462-gov-bb.dat rename to system/quests/q462-gv2-bb.dat diff --git a/system/quests/q463-gov-bb.bin b/system/quests/q463-gv2-bb.bin similarity index 100% rename from system/quests/q463-gov-bb.bin rename to system/quests/q463-gv2-bb.bin diff --git a/system/quests/q463-gov-bb.dat b/system/quests/q463-gv2-bb.dat similarity index 100% rename from system/quests/q463-gov-bb.dat rename to system/quests/q463-gv2-bb.dat diff --git a/system/quests/q464-gov-bb.bin b/system/quests/q464-gv2-bb.bin similarity index 100% rename from system/quests/q464-gov-bb.bin rename to system/quests/q464-gv2-bb.bin diff --git a/system/quests/q464-gov-bb.dat b/system/quests/q464-gv2-bb.dat similarity index 100% rename from system/quests/q464-gov-bb.dat rename to system/quests/q464-gv2-bb.dat diff --git a/system/quests/q465-gov-bb.bin b/system/quests/q465-gv2-bb.bin similarity index 100% rename from system/quests/q465-gov-bb.bin rename to system/quests/q465-gv2-bb.bin diff --git a/system/quests/q465-gov-bb.dat b/system/quests/q465-gv2-bb.dat similarity index 100% rename from system/quests/q465-gov-bb.dat rename to system/quests/q465-gv2-bb.dat diff --git a/system/quests/q466-gov-bb.bin b/system/quests/q466-gv2-bb.bin similarity index 100% rename from system/quests/q466-gov-bb.bin rename to system/quests/q466-gv2-bb.bin diff --git a/system/quests/q466-gov-bb.dat b/system/quests/q466-gv2-bb.dat similarity index 100% rename from system/quests/q466-gov-bb.dat rename to system/quests/q466-gv2-bb.dat diff --git a/system/quests/q467-gov-bb.bin b/system/quests/q467-gv2-bb.bin similarity index 100% rename from system/quests/q467-gov-bb.bin rename to system/quests/q467-gv2-bb.bin diff --git a/system/quests/q467-gov-bb.dat b/system/quests/q467-gv2-bb.dat similarity index 100% rename from system/quests/q467-gov-bb.dat rename to system/quests/q467-gv2-bb.dat diff --git a/system/quests/q468-gov-bb.bin b/system/quests/q468-gv2-bb.bin similarity index 100% rename from system/quests/q468-gov-bb.bin rename to system/quests/q468-gv2-bb.bin diff --git a/system/quests/q468-gov-bb.dat b/system/quests/q468-gv2-bb.dat similarity index 100% rename from system/quests/q468-gov-bb.dat rename to system/quests/q468-gv2-bb.dat diff --git a/system/quests/q701-gov-bb.bin b/system/quests/q701-gv4-bb.bin similarity index 100% rename from system/quests/q701-gov-bb.bin rename to system/quests/q701-gv4-bb.bin diff --git a/system/quests/q701-gov-bb.dat b/system/quests/q701-gv4-bb.dat similarity index 100% rename from system/quests/q701-gov-bb.dat rename to system/quests/q701-gv4-bb.dat diff --git a/system/quests/q702-gov-bb.bin b/system/quests/q702-gv4-bb.bin similarity index 100% rename from system/quests/q702-gov-bb.bin rename to system/quests/q702-gv4-bb.bin diff --git a/system/quests/q702-gov-bb.dat b/system/quests/q702-gv4-bb.dat similarity index 100% rename from system/quests/q702-gov-bb.dat rename to system/quests/q702-gv4-bb.dat diff --git a/system/quests/q703-gov-bb.bin b/system/quests/q703-gv4-bb.bin similarity index 100% rename from system/quests/q703-gov-bb.bin rename to system/quests/q703-gv4-bb.bin diff --git a/system/quests/q703-gov-bb.dat b/system/quests/q703-gv4-bb.dat similarity index 100% rename from system/quests/q703-gov-bb.dat rename to system/quests/q703-gv4-bb.dat diff --git a/system/quests/q704-gov-bb.bin b/system/quests/q704-gv4-bb.bin similarity index 100% rename from system/quests/q704-gov-bb.bin rename to system/quests/q704-gv4-bb.bin diff --git a/system/quests/q704-gov-bb.dat b/system/quests/q704-gv4-bb.dat similarity index 100% rename from system/quests/q704-gov-bb.dat rename to system/quests/q704-gv4-bb.dat diff --git a/system/quests/q705-gov-bb.bin b/system/quests/q705-gv4-bb.bin similarity index 100% rename from system/quests/q705-gov-bb.bin rename to system/quests/q705-gv4-bb.bin diff --git a/system/quests/q705-gov-bb.dat b/system/quests/q705-gv4-bb.dat similarity index 100% rename from system/quests/q705-gov-bb.dat rename to system/quests/q705-gv4-bb.dat diff --git a/system/quests/q706-gov-bb.bin b/system/quests/q706-gv4-bb.bin similarity index 100% rename from system/quests/q706-gov-bb.bin rename to system/quests/q706-gv4-bb.bin diff --git a/system/quests/q706-gov-bb.dat b/system/quests/q706-gv4-bb.dat similarity index 100% rename from system/quests/q706-gov-bb.dat rename to system/quests/q706-gv4-bb.dat diff --git a/system/quests/q707-gov-bb.bin b/system/quests/q707-gv4-bb.bin similarity index 100% rename from system/quests/q707-gov-bb.bin rename to system/quests/q707-gv4-bb.bin diff --git a/system/quests/q707-gov-bb.dat b/system/quests/q707-gv4-bb.dat similarity index 100% rename from system/quests/q707-gov-bb.dat rename to system/quests/q707-gv4-bb.dat diff --git a/system/quests/q708-gov-bb.bin b/system/quests/q708-gv4-bb.bin similarity index 100% rename from system/quests/q708-gov-bb.bin rename to system/quests/q708-gv4-bb.bin diff --git a/system/quests/q708-gov-bb.dat b/system/quests/q708-gv4-bb.dat similarity index 100% rename from system/quests/q708-gov-bb.dat rename to system/quests/q708-gv4-bb.dat diff --git a/system/quests/q709-gov-bb.bin b/system/quests/q709-gv4-bb.bin similarity index 100% rename from system/quests/q709-gov-bb.bin rename to system/quests/q709-gv4-bb.bin diff --git a/system/quests/q709-gov-bb.dat b/system/quests/q709-gv4-bb.dat similarity index 100% rename from system/quests/q709-gov-bb.dat rename to system/quests/q709-gv4-bb.dat diff --git a/tests/config.json b/tests/config.json index 6e58e0cd..8cc8b72d 100644 --- a/tests/config.json +++ b/tests/config.json @@ -97,55 +97,41 @@ "PCPatchServerMessage": "newserv patch server\r\n\r\nThis server is not affiliated with, sponsored by, or in any other way connected to SEGA or Sonic Team, and is owned and operated completely independently.", "BBPatchServerMessage": "$C7newserv patch server\n\nThis server is not affiliated with, sponsored by, or in any\nother way connected to SEGA or Sonic Team, and is owned\nand operated completely independently.", - "CommonItemDropRates-Enemy": [ - 0x03000000, // material - 0x20000000, // equipment - 0x06000000, // technique disk - 0x01800000, // scape doll - 0x06000000, // grinder - 0x10000000, // atomizers, etc. - 0x20000000, // mates/fluids - 0x40000000, // meseta - ], - "CommonItemDropRates-Box": [ - 0x00800000, // material - 0x20000000, // equipment - 0x01000000, // technique disk - 0x02000000, // scape doll - 0x08000000, // grinder - 0x10000000, // atomizers, etc. - 0x20000000, // mates/fluids - 0x80000000, // meseta - ], - - "CommonUnitTypes": [ - // normal - [0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x08, 0x08, 0x08, 0x0C, 0x0C, 0x0C, - 0x10, 0x10, 0x10, 0x14, 0x14, 0x14, 0x18, 0x18, 0x18, 0x21, 0x21, 0x21, - 0x24, 0x24, 0x24, 0x27, 0x27, 0x27, 0x2A, 0x2A, 0x2A, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF], - // hard - [0x01, 0x01, 0x05, 0x05, 0x09, 0x09, 0x0D, 0x0D, 0x11, 0x11, 0x15, 0x15, - 0x19, 0x19, 0x21, 0x21, 0x24, 0x24, 0x27, 0x27, 0x2A, 0x2A, 0x30, 0x30, - 0x33, 0x33, 0x36, 0x36, 0x39, 0x39, 0x3C, 0x3C, 0x3F, 0x3F, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF], - // very hard - [0x02, 0x02, 0x06, 0x06, 0x0A, 0x0A, 0x0E, 0x0E, 0x12, 0x12, 0x16, 0x16, - 0x1A, 0x1A, 0x22, 0x22, 0x25, 0x25, 0x28, 0x28, 0x2B, 0x2B, 0x31, 0x31, - 0x34, 0x34, 0x37, 0x37, 0x3A, 0x3A, 0x3D, 0x3D, 0x3F, 0x3F, 0x41, 0x41, - 0x42, 0x42, 0x43, 0x43, 0x44, 0x44, 0x45, 0x45, 0x46, 0x46, 0x47, 0x47, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF], - // ultimate - [0x02, 0x02, 0x06, 0x06, 0x0A, 0x0A, 0x0E, 0x0E, 0x12, 0x12, 0x16, 0x16, - 0x1A, 0x1A, 0x23, 0x23, 0x26, 0x26, 0x29, 0x29, 0x2C, 0x2C, 0x31, 0x31, - 0x34, 0x34, 0x37, 0x37, 0x3A, 0x3A, 0x3D, 0x3D, 0x3F, 0x3F, 0x41, 0x41, - 0x42, 0x42, 0x43, 0x43, 0x44, 0x44, 0x45, 0x45, 0x46, 0x46, 0x47, 0x47, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF], + "QuestCategories": [ + // Each entry here is [type, token, flags, category_name, description]. The + // order these are defined matches the order they'll appear in the quest + // categories menu on the client. + // The fields in each entry are: + // flags: a bit field containing an OR'ed combination of one or more of: + // 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 and PC) + // type: the character that newserv expects at the beginning of the quest + // filename + // 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"], ], }