diff --git a/src/Menu.hh b/src/Menu.hh index 12608b7d..589dc685 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -18,7 +18,7 @@ constexpr uint32_t INFORMATION = 0x22000022; constexpr uint32_t LOBBY = 0x33000033; constexpr uint32_t GAME = 0x44000044; constexpr uint32_t QUEST = 0x55000055; -constexpr uint32_t QUEST_FILTER = 0x66000066; +constexpr uint32_t QUEST_CATEGORIES = 0x66000066; constexpr uint32_t PROXY_DESTINATIONS = 0x77000077; constexpr uint32_t PROGRAMS = 0x88000088; constexpr uint32_t PATCHES = 0x99000099; diff --git a/src/Quest.cc b/src/Quest.cc index 9d9a4c15..0e4c3796 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -23,29 +23,20 @@ using namespace std; QuestCategoryIndex::Category::Category(uint32_t category_id, const JSON& json) : category_id(category_id) { - this->flags = json.get_int(0); + this->enabled_flags = json.get_int(0); this->directory_name = json.get_string(1); this->name = json.get_string(2); this->description = json.get_string(3); } -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(const JSON& json) { uint32_t next_category_id = 1; for (const auto& it : json.as_list()) { - this->categories.emplace_back(next_category_id++, *it); + this->categories.emplace_back(new Category(next_category_id++, *it)); } } -const QuestCategoryIndex::Category& QuestCategoryIndex::at(uint32_t category_id) const { +shared_ptr QuestCategoryIndex::at(uint32_t category_id) const { return this->categories.at(category_id - 1); } @@ -460,12 +451,12 @@ QuestIndex::QuestIndex( for (const auto& cat : this->category_index->categories) { // Don't index Ep3 download categories for non-Ep3 quest indexing, and vice // versa - if (is_ep3 == !(cat.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD)) { + if (is_ep3 != cat->check_flag(QuestMenuType::EP3_DOWNLOAD)) { continue; } auto add_file = [&](map>& files, const string& name, string&& value) { - if (categories.emplace(name, cat.category_id).first->second != cat.category_id) { + if (categories.emplace(name, cat->category_id).first->second != cat->category_id) { throw runtime_error("file " + name + " exists in multiple categories"); } shared_ptr data_ptr(new string(std::move(value))); @@ -474,7 +465,7 @@ QuestIndex::QuestIndex( } }; - string cat_path = directory + "/" + cat.directory_name; + string cat_path = directory + "/" + cat->directory_name; if (!isdir(cat_path)) { static_game_data_log.warning("Quest category directory %s is missing; skipping it", cat_path.c_str()); continue; @@ -673,7 +664,7 @@ QuestIndex::QuestIndex( battle_rules, challenge_template_index)); - auto category_name = this->category_index->at(vq->category_id).name; + auto category_name = this->category_index->at(vq->category_id)->name; string dat_str = dat_filename.empty() ? "" : (" with layout from " + dat_filename + ".dat"); string battle_rules_str = battle_rules ? (" with battle rules from " + json_filename + ".json") : ""; string challenge_template_str = (challenge_template_index >= 0) ? string_printf(" with challenge template index %zd", vq->challenge_template_index) : ""; @@ -719,16 +710,47 @@ shared_ptr QuestIndex::get(uint32_t quest_number) const { } } -vector> QuestIndex::filter(Episode episode, uint32_t category_id, Version version) const { - vector> ret; - for (auto it : this->quests_by_number) { - if (((episode == Episode::NONE) || (it.second->episode == episode)) && - (it.second->category_id == category_id) && - it.second->has_version_any_language(version)) { - ret.emplace_back(it.second); - } +const vector>& QuestIndex::categories( + QuestMenuType menu_type, Episode episode, Version version) const { + // The episode filter should apply in normal or solo mode + if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) { + episode = Episode::NONE; + } + + uint64_t key = (static_cast(menu_type) << 20) | (static_cast(episode) << 16) | static_cast(version); + try { + return this->category_filter_results_cache.at(key); + } catch (const out_of_range&) { + auto& ret = this->category_filter_results_cache[key]; + for (const auto& cat : this->category_index->categories) { + if (cat->check_flag(menu_type) && !this->filter(menu_type, episode, version, cat->category_id).empty()) { + ret.emplace_back(cat); + } + } + return ret; + } +} + +const vector>& QuestIndex::filter( + QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const { + if ((menu_type != QuestMenuType::NORMAL) && (menu_type != QuestMenuType::SOLO)) { + episode = Episode::NONE; + } + + uint64_t key = (static_cast(episode) << 48) | (static_cast(version) << 32) | category_id; + try { + return this->quest_filter_results_cache.at(key); + } catch (const out_of_range&) { + vector>& ret = this->quest_filter_results_cache[key]; + for (auto it : this->quests_by_number) { + if (((episode == Episode::NONE) || (it.second->episode == episode)) && + (it.second->category_id == category_id) && + it.second->has_version_any_language(version)) { + ret.emplace_back(it.second); + } + } + return ret; } - return ret; } string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) { diff --git a/src/Quest.hh b/src/Quest.hh index 40b5364b..60f6eb2b 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -21,35 +21,36 @@ enum class QuestFileFormat { QST, }; +enum class QuestMenuType { + NORMAL = 0, + BATTLE = 1, + CHALLENGE = 2, + SOLO = 3, + GOVERNMENT = 4, + DOWNLOAD = 5, + EP3_DOWNLOAD = 6, +}; + 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, - }; - uint32_t category_id; - uint8_t flags; + uint8_t enabled_flags; std::string directory_name; std::string name; std::string description; explicit Category(uint32_t category_id, const JSON& json); - bool matches_flags(uint8_t request) const; + [[nodiscard]] inline bool check_flag(QuestMenuType menu_type) const { + return this->enabled_flags & (1 << static_cast(menu_type)); + } }; - std::vector categories; + std::vector> categories; explicit QuestCategoryIndex(const JSON& json); - const Category& at(uint32_t category_id) const; + std::shared_ptr at(uint32_t category_id) const; }; struct VersionedQuest { @@ -120,10 +121,17 @@ struct QuestIndex { std::map> quests_by_number; + mutable std::unordered_map>> category_filter_results_cache; + mutable std::unordered_map>> quest_filter_results_cache; + QuestIndex(const std::string& directory, std::shared_ptr category_index, bool is_ep3); std::shared_ptr get(uint32_t quest_number) const; - std::vector> filter(Episode episode, uint32_t category_id, Version version) const; + + const std::vector>& categories( + QuestMenuType menu_type, Episode episode, Version version) const; + const std::vector>& filter( + QuestMenuType menu_type, Episode episode, Version version, uint32_t category_id) const; }; std::string encode_download_quest_data( diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3137eaa5..7a532414 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1653,7 +1653,7 @@ static void on_09(shared_ptr c, uint16_t, uint32_t, string& data) { auto s = c->require_server_state(); switch (cmd.menu_id) { - case MenuID::QUEST_FILTER: + case MenuID::QUEST_CATEGORIES: // Don't send anything here. The quest filter menu already has short // descriptions included with the entries, which the client shows in the // usual location on the screen. @@ -2043,40 +2043,23 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { case MainMenuItemID::DOWNLOAD_QUESTS: { auto s = c->require_server_state(); + QuestMenuType menu_type = QuestMenuType::DOWNLOAD; if (is_ep3(c->version())) { + menu_type = QuestMenuType::EP3_DOWNLOAD; // 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.) - uint32_t ep3_category_id = 0; - size_t num_ep3_categories = 0; - for (const auto& category : s->quest_category_index->categories) { - if (category.flags & QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD) { - ep3_category_id = category.category_id; - num_ep3_categories++; - } - } - if (num_ep3_categories == 1) { - auto quest_index = s->quest_index_for_client(c); - if (quest_index) { - auto quests = quest_index->filter(Episode::EP3, ep3_category_id, c->version()); - send_quest_menu(c, MenuID::QUEST, quests, true); - } else { - send_lobby_message_box(c, "$C6Quests are not available."); - } + auto quest_index = s->quest_index_for_client(c); + const auto& categories = quest_index->categories(menu_type, Episode::EP3, c->version()); + if (categories.size() == 1) { + auto quests = quest_index->filter(menu_type, Episode::EP3, c->version(), categories[0]->category_id); + send_quest_menu(c, MenuID::QUEST, quests, true); break; } } - // Not Episode 3, or there are multiple Episode 3 download categories; - // send the categories menu instead - uint8_t flags = is_ep3(c->version()) - ? QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD - : QuestCategoryIndex::Category::Flag::DOWNLOAD; - if (is_v1_or_v2(c->version())) { - flags |= QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3; - } - send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags); + send_quest_categories_menu(c, MenuID::QUEST_CATEGORIES, s->quest_index_for_client(c), menu_type, Episode::NONE); break; } @@ -2304,20 +2287,38 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { break; } - case MenuID::QUEST_FILTER: { + case MenuID::QUEST_CATEGORIES: { auto s = c->require_server_state(); auto quest_index = s->quest_index_for_client(c); if (!quest_index) { send_lobby_message_box(c, "$C6Quests are not available."); break; } - const auto& category = s->quest_category_index->at(item_id); - shared_ptr l = c->lobby.lock(); - bool filter_by_episode = l && !(category.flags & QuestCategoryIndex::Category::Flag::GOVERNMENT); - auto quests = quest_index->filter(filter_by_episode ? l->episode : Episode::NONE, item_id, c->version()); - // Hack: Assume the menu to be sent is the download quest menu if the - // client is not in any lobby + shared_ptr l = c->lobby.lock(); + Episode episode = l ? l->episode : Episode::NONE; + QuestMenuType menu_type = QuestMenuType::NORMAL; + if (!l) { + // Assume the menu to be sent is the download quest menu if the client + // is not in any lobby + menu_type = is_ep3(c->version()) ? QuestMenuType::EP3_DOWNLOAD : QuestMenuType::DOWNLOAD; + } else { + auto cat = quest_index->category_index->at(item_id); + static const std::array menu_types({ + QuestMenuType::GOVERNMENT, + QuestMenuType::CHALLENGE, + QuestMenuType::BATTLE, + QuestMenuType::SOLO, + }); + for (QuestMenuType check_menu_type : menu_types) { + if (cat->check_flag(check_menu_type)) { + menu_type = check_menu_type; + break; + } + } + } + + const auto& quests = quest_index->filter(menu_type, episode, c->version(), item_id); send_quest_menu(c, MenuID::QUEST, quests, !l); break; } @@ -2647,34 +2648,29 @@ static void on_A2(shared_ptr c, uint16_t, uint32_t flag, string& data) { // filter menu. if (is_ep3(c->version())) { send_lobby_message_box(c, "$C6Episode 3 does not\nprovide online quests\nvia this interface."); - } else { - uint8_t flags = is_v1_or_v2(c->version()) - ? QuestCategoryIndex::Category::Flag::HIDE_ON_PRE_V3 - : 0; - + QuestMenuType menu_type; if ((c->version() == Version::BB_V4) && flag) { - flags |= QuestCategoryIndex::Category::Flag::GOVERNMENT; + menu_type = QuestMenuType::GOVERNMENT; } else { switch (l->mode) { case GameMode::NORMAL: - flags |= QuestCategoryIndex::Category::Flag::NORMAL; + menu_type = QuestMenuType::NORMAL; break; case GameMode::BATTLE: - flags |= QuestCategoryIndex::Category::Flag::BATTLE; + menu_type = QuestMenuType::BATTLE; break; case GameMode::CHALLENGE: - flags |= QuestCategoryIndex::Category::Flag::CHALLENGE; + menu_type = QuestMenuType::CHALLENGE; break; case GameMode::SOLO: - flags |= QuestCategoryIndex::Category::Flag::SOLO; + menu_type = QuestMenuType::SOLO; break; default: throw logic_error("invalid game mode"); } } - - send_quest_menu(c, MenuID::QUEST_FILTER, s->quest_category_index, flags); + send_quest_categories_menu(c, MenuID::QUEST_CATEGORIES, s->quest_index_for_client(c), menu_type, l->episode); } } @@ -4000,6 +3996,15 @@ static void on_C1_BB(shared_ptr c, uint16_t, uint32_t, string& data) { break; case 3: episode = Episode::EP4; + // Disallow battle/challenge in Ep4 + if (mode == GameMode::BATTLE) { + send_lobby_message_box(c, "$C6Episode 4 does not\nsupport Battle Mode."); + return; + } + if (mode == GameMode::CHALLENGE) { + send_lobby_message_box(c, "$C6Episode 4 does not\nsupport Challenge Mode."); + return; + } break; default: throw runtime_error("invalid episode number"); diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 33c1e76e..e8a2dea8 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -1394,23 +1394,22 @@ void send_quest_menu_t( } template -void send_quest_menu_t( +void send_quest_categories_menu_t( shared_ptr c, uint32_t menu_id, - shared_ptr category_index, - uint8_t flags) { - bool is_download_menu = flags & (QuestCategoryIndex::Category::Flag::DOWNLOAD | QuestCategoryIndex::Category::Flag::EP3_DOWNLOAD); + shared_ptr quest_index, + QuestMenuType menu_type, + Episode episode) { vector entries; - for (const auto& category : category_index->categories) { - if (!category.matches_flags(flags)) { - continue; - } + for (const auto& cat : quest_index->categories(menu_type, episode, c->version())) { auto& e = entries.emplace_back(); e.menu_id = menu_id; - e.item_id = category.category_id; - e.name.encode(category.name, c->language()); - e.short_description.encode(add_color(category.description), c->language()); + e.item_id = cat->category_id; + e.name.encode(cat->name, c->language()); + e.short_description.encode(add_color(cat->description), c->language()); } + + bool is_download_menu = (menu_type == QuestMenuType::DOWNLOAD) || (menu_type == QuestMenuType::EP3_DOWNLOAD); send_command_vt(c, is_download_menu ? 0xA4 : 0xA2, entries.size(), entries); } @@ -1441,11 +1440,11 @@ void send_quest_menu(shared_ptr c, uint32_t menu_id, } } -void send_quest_menu(shared_ptr c, uint32_t menu_id, - shared_ptr category_index, uint8_t flags) { +void send_quest_categories_menu(shared_ptr c, uint32_t menu_id, + shared_ptr quest_index, QuestMenuType menu_type, Episode episode) { switch (c->version()) { case Version::PC_V2: - send_quest_menu_t(c, menu_id, category_index, flags); + send_quest_categories_menu_t(c, menu_id, quest_index, menu_type, episode); break; case Version::DC_NTE: case Version::DC_V1_12_2000_PROTOTYPE: @@ -1455,13 +1454,13 @@ void send_quest_menu(shared_ptr c, uint32_t menu_id, case Version::GC_V3: case Version::GC_EP3_TRIAL_EDITION: case Version::GC_EP3: - send_quest_menu_t(c, menu_id, category_index, flags); + send_quest_categories_menu_t(c, menu_id, quest_index, menu_type, episode); break; case Version::XB_V3: - send_quest_menu_t(c, menu_id, category_index, flags); + send_quest_categories_menu_t(c, menu_id, quest_index, menu_type, episode); break; case Version::BB_V4: - send_quest_menu_t(c, menu_id, category_index, flags); + send_quest_categories_menu_t(c, menu_id, quest_index, menu_type, episode); break; default: throw logic_error("unimplemented versioned command"); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index f93e6b9e..ca46752e 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -253,10 +253,17 @@ void send_game_menu( std::shared_ptr c, bool is_spectator_team_list, bool is_tournament_game_list); -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, - std::shared_ptr category_index, uint8_t flags); +void send_quest_menu( + std::shared_ptr c, + uint32_t menu_id, + const std::vector>& quests, + bool is_download_menu); +void send_quest_categories_menu( + std::shared_ptr c, + uint32_t menu_id, + std::shared_ptr quest_index, + QuestMenuType menu_type, + Episode episode); void send_lobby_list(std::shared_ptr c); void send_player_records(std::shared_ptr c, std::shared_ptr l, std::shared_ptr joining_client = nullptr); diff --git a/system/config.example.json b/system/config.example.json index 1403605d..c6af4123 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -482,7 +482,6 @@ // 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) // directory_name: the directory inside system/quests that contains quests // for this category. // category_name: what appears in the quest menu on the client. @@ -493,8 +492,8 @@ [0x01, "events", "Events", "$E$C6Quests that are part\nof an event"], [0x01, "shops", "Shops", "$E$C6Quests that contain\nshops"], [0x01, "vr", "Virtual Reality", "$E$C6Quests that are\ndone in a simulator"], - [0x81, "tower", "Control Tower", "$E$C6Quests that take\nplace at the Control\nTower"], - [0x81, "team", "Team", "$E$C6Quests for you\nand your team\nmembers."], + [0x01, "tower", "Control Tower", "$E$C6Quests that take\nplace at the Control\nTower"], + [0x01, "team", "Team", "$E$C6Quests for you\nand your team\nmembers."], [0x02, "battle", "Battle", "$E$C6Battle mode rule\nsets"], [0x04, "challenge-ep1", "Challenge (Episode 1)", "$E$C6Challenge mode\nquests in Episode 1"], [0x84, "challenge-ep2", "Challenge (Episode 2)", "$E$C6Challenge mode\nquests in Episode 2"],