diff --git a/README.md b/README.md index 766b6337..08ae82a4 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,10 @@ For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-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`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/. -Some quests (mostly battle and challenge mode quests) have additional JSON metadata files that describe how the server should handle them. These files include flags that can be used to hide the quest unless a preceding quest has been cleared, or to hide the quest unless purchased as a BB team reward. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See system/quests/battle/b88001.json for documentation on the exact format of the JSON file. +Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/battle/b88001.json for all of the available options and how to use them. Some of the options are: +- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met +- Enable the quest to be joined while in progress +- Override the common and/or rare item tables and set the allowed drop modes Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts. diff --git a/src/AFSArchive.hh b/src/AFSArchive.hh index 38df53d4..292be2a4 100644 --- a/src/AFSArchive.hh +++ b/src/AFSArchive.hh @@ -23,6 +23,10 @@ public: return this->entries; } + inline size_t num_entries() const { + return this->entries.size(); + } + std::pair get(size_t index) const; std::string get_copy(size_t index) const; phosg::StringReader get_reader(size_t index) const; diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 630c9f04..1dddd1c3 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -785,41 +785,40 @@ ChatCommandDefinition cc_dropmode( if (a.c->proxy_session) { - using DropMode = ProxySession::DropMode; if (a.text.empty()) { switch (a.c->proxy_session->drop_mode) { - case DropMode::DISABLED: + case ProxyDropMode::DISABLED: send_text_message(a.c, "Drop mode: disabled"); break; - case DropMode::PASSTHROUGH: + case ProxyDropMode::PASSTHROUGH: send_text_message(a.c, "Drop mode: default"); break; - case DropMode::INTERCEPT: + case ProxyDropMode::INTERCEPT: send_text_message(a.c, "Drop mode: proxy"); break; } } else { - DropMode new_mode; + ProxyDropMode new_mode; if ((a.text == "none") || (a.text == "disabled")) { - new_mode = DropMode::DISABLED; + new_mode = ProxyDropMode::DISABLED; } else if ((a.text == "default") || (a.text == "passthrough")) { - new_mode = DropMode::PASSTHROUGH; + new_mode = ProxyDropMode::PASSTHROUGH; } else if ((a.text == "proxy") || (a.text == "intercept")) { - new_mode = DropMode::INTERCEPT; + new_mode = ProxyDropMode::INTERCEPT; } else { throw precondition_failed("Invalid drop mode"); } a.c->proxy_session->set_drop_mode(s, a.c->version(), a.c->override_random_seed, new_mode); switch (a.c->proxy_session->drop_mode) { - case DropMode::DISABLED: + case ProxyDropMode::DISABLED: send_text_message(a.c->channel, "Item drops disabled"); break; - case DropMode::PASSTHROUGH: + case ProxyDropMode::PASSTHROUGH: send_text_message(a.c->channel, "Item drops changed\nto default mode"); break; - case DropMode::INTERCEPT: + case ProxyDropMode::INTERCEPT: send_text_message(a.c->channel, "Item drops changed\nto proxy mode"); break; } @@ -829,36 +828,36 @@ ChatCommandDefinition cc_dropmode( auto l = a.c->require_lobby(); if (a.text.empty()) { switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: send_text_message(a.c, "Drop mode: disabled"); break; - case Lobby::DropMode::CLIENT: + case ServerDropMode::CLIENT: send_text_message(a.c, "Drop mode: client"); break; - case Lobby::DropMode::SERVER_SHARED: + case ServerDropMode::SERVER_SHARED: send_text_message(a.c, "Drop mode: server\nshared"); break; - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_PRIVATE: send_text_message(a.c, "Drop mode: server\nprivate"); break; - case Lobby::DropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_DUPLICATE: send_text_message(a.c, "Drop mode: server\nduplicate"); break; } } else { a.check_is_leader(); - Lobby::DropMode new_mode; + ServerDropMode new_mode; if ((a.text == "none") || (a.text == "disabled")) { - new_mode = Lobby::DropMode::DISABLED; + new_mode = ServerDropMode::DISABLED; } else if (a.text == "client") { - new_mode = Lobby::DropMode::CLIENT; + new_mode = ServerDropMode::CLIENT; } else if ((a.text == "shared") || (a.text == "server")) { - new_mode = Lobby::DropMode::SERVER_SHARED; + new_mode = ServerDropMode::SERVER_SHARED; } else if ((a.text == "private") || (a.text == "priv")) { - new_mode = Lobby::DropMode::SERVER_PRIVATE; + new_mode = ServerDropMode::SERVER_PRIVATE; } else if ((a.text == "duplicate") || (a.text == "dup")) { - new_mode = Lobby::DropMode::SERVER_DUPLICATE; + new_mode = ServerDropMode::SERVER_DUPLICATE; } else { throw precondition_failed("Invalid drop mode"); } @@ -869,19 +868,19 @@ ChatCommandDefinition cc_dropmode( l->drop_mode = new_mode; switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: send_text_message(l, "Item drops disabled"); break; - case Lobby::DropMode::CLIENT: + case ServerDropMode::CLIENT: send_text_message(l, "Item drops changed\nto client mode"); break; - case Lobby::DropMode::SERVER_SHARED: + case ServerDropMode::SERVER_SHARED: send_text_message(l, "Item drops changed\nto server shared\nmode"); break; - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_PRIVATE: send_text_message(l, "Item drops changed\nto server private\nmode"); break; - case Lobby::DropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_DUPLICATE: send_text_message(l, "Item drops changed\nto server duplicate\nmode"); break; } @@ -1262,7 +1261,7 @@ ChatCommandDefinition cc_item( item = s->parse_item_description(a.c->version(), a.text); item.id = l->generate_item_id(a.c->lobby_client_id); - if ((l->drop_mode == Lobby::DropMode::SERVER_PRIVATE) || (l->drop_mode == Lobby::DropMode::SERVER_DUPLICATE)) { + if ((l->drop_mode == ServerDropMode::SERVER_PRIVATE) || (l->drop_mode == ServerDropMode::SERVER_DUPLICATE)) { l->add_item(a.c->floor, item, a.c->pos, nullptr, nullptr, (1 << a.c->lobby_client_id)); send_drop_stacked_item_to_channel(s, a.c->channel, item, a.c->floor, a.c->pos); } else { @@ -1452,19 +1451,19 @@ ChatCommandDefinition cc_lobby_info( "$C7Section ID: $C6{}$C7", name_for_section_id(l->effective_section_id()))); switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: lines.emplace_back("Drops disabled"); break; - case Lobby::DropMode::CLIENT: + case ServerDropMode::CLIENT: lines.emplace_back("Client item table"); break; - case Lobby::DropMode::SERVER_SHARED: + case ServerDropMode::SERVER_SHARED: lines.emplace_back("Server item table"); break; - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_PRIVATE: lines.emplace_back("Server indiv items"); break; - case Lobby::DropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_DUPLICATE: lines.emplace_back("Server dup items"); break; default: diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index d78894e0..fb6ce11c 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -435,9 +435,9 @@ struct C_LegacyLogin_BB_04 { // 05 = Server down for maintenance (108) // 06 = Incorrect password (127) // Any other nonzero value = Generic failure (101) -// The client config field in this command is ignored by pre-V3 clients as well -// as Episodes 1&2 Trial Edition. All other V3 clients save it as opaque data to -// be returned in a 9E or 9F command later. +// The client config field in this command is ignored by all clients that never +// send 9E. Clients that do send 9E will save thie client config as opaque data +// to be returned in a 9E or 9F command later. // The client will respond with a 96 command, but only the first time it // receives this command - for later 04 commands, the client will still update // its client config but will not respond. Changing the security data at any @@ -4298,7 +4298,8 @@ struct G_FeedMag_6x28 { le_uint32_t fed_item_id = 0; } __packed_ws__(G_FeedMag_6x28, 0x0C); -// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) (protected on V3 but not V4) +// 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) +// (protected on V3 but not on V4) // This subcommand is also used for reducing the size of stacks - if amount is // less than the stack count, the item is not deleted and its ID remains valid. @@ -5739,14 +5740,16 @@ struct G_SetLobbyChairState_6xAE { le_float unknown_a4 = 0; } __packed_ws__(G_SetLobbyChairState_6xAE, 0x10); -// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) +// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected +// on V3/V4) struct G_TurnLobbyChair_6xAF { G_ClientIDHeader header; le_uint32_t angle = 0; // In range [0x0000, 0xFFFF] } __packed_ws__(G_TurnLobbyChair_6xAF, 8); -// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4) +// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected +// on V3/V4) struct G_MoveLobbyChair_6xB0 { G_ClientIDHeader header; diff --git a/src/CommonItemSet.cc b/src/CommonItemSet.cc index 209afd0c..0d8a8bdc 100644 --- a/src/CommonItemSet.cc +++ b/src/CommonItemSet.cc @@ -677,29 +677,49 @@ shared_ptr CommonItemSet::get_table( } AFSV2CommonItemSet::AFSV2CommonItemSet( - std::shared_ptr pt_afs_data, - std::shared_ptr ct_afs_data) { - // ItemPT.afs has 40 entries; the first 10 are for Normal, then Hard, etc. - AFSArchive pt_afs(pt_afs_data); - for (size_t difficulty = 0; difficulty < 4; difficulty++) { - for (size_t section_id = 0; section_id < 10; section_id++) { - auto entry = pt_afs.get(difficulty * 10 + section_id); - phosg::StringReader r(entry.first, entry.second); - auto table = make_shared(r, false, false, Episode::EP1); - this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table); - this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table); - this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table); + std::shared_ptr pt_afs_data, std::shared_ptr ct_afs_data) { + // Each AFS file has 40 entries (30 on v1); the first 10 are for Normal, then + // Hard, etc. + { + AFSArchive pt_afs(pt_afs_data); + size_t max_difficulty; + if (pt_afs.num_entries() >= 40) { + max_difficulty = 4; + } else if (pt_afs.num_entries() >= 30) { + max_difficulty = 3; + } else { + throw std::runtime_error(std::format("PT AFS file has unexpected entry count ({})", pt_afs.num_entries())); + } + for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) { + for (size_t section_id = 0; section_id < 10; section_id++) { + auto entry = pt_afs.get(difficulty * 10 + section_id); + phosg::StringReader r(entry.first, entry.second); + auto table = make_shared
(r, false, false, Episode::EP1); + this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::NORMAL, difficulty, section_id), table); + this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::BATTLE, difficulty, section_id), table); + this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::SOLO, difficulty, section_id), table); + } } } - // ItemCT.afs also has 40 entries, but only the 0th, 10th, 20th, and 30th are - // used (section_id is ignored) - AFSArchive ct_afs(ct_afs_data); - for (size_t difficulty = 0; difficulty < 4; difficulty++) { - auto r = ct_afs.get_reader(difficulty * 10); - auto table = make_shared
(r, false, false, Episode::EP1); - for (size_t section_id = 0; section_id < 10; section_id++) { - this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table); + // ItemCT AFS files also have 40 entries, but only the 0th, 10th, 20th, and + // 30th are used (section_id is ignored) + if (ct_afs_data) { + AFSArchive ct_afs(ct_afs_data); + size_t max_difficulty; + if (ct_afs.num_entries() >= 40) { + max_difficulty = 4; + } else if (ct_afs.num_entries() >= 30) { + max_difficulty = 3; + } else { + throw std::runtime_error(std::format("CT AFS file has unexpected entry count ({})", ct_afs.num_entries())); + } + for (size_t difficulty = 0; difficulty < max_difficulty; difficulty++) { + auto r = ct_afs.get_reader(difficulty * 10); + auto table = make_shared
(r, false, false, Episode::EP1); + for (size_t section_id = 0; section_id < 10; section_id++) { + this->tables.emplace(this->key_for_table(Episode::EP1, GameMode::CHALLENGE, difficulty, section_id), table); + } } } } @@ -758,10 +778,14 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr gs if (episode != Episode::EP4) { for (size_t difficulty = 0; difficulty < 4; difficulty++) { - auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true)); - auto table = make_shared
(r, is_big_endian, true, episode); - for (size_t section_id = 0; section_id < 10; section_id++) { - this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table); + try { + auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true)); + auto table = make_shared
(r, is_big_endian, true, episode); + for (size_t section_id = 0; section_id < 10; section_id++) { + this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table); + } + } catch (const out_of_range&) { + // GC NTE doesn't have Ep2 challenge; just skip adding the table } } } diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 9d52b966..63543dbf 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -328,13 +328,13 @@ std::shared_ptr HTTPServer::generate_client_json( {"LobbyPlayers", std::move(lobby_players_json)}, }); switch (ses->drop_mode) { - case ProxySession::DropMode::DISABLED: + case ProxyDropMode::DISABLED: ses_json.emplace("DropMode", "none"); break; - case ProxySession::DropMode::PASSTHROUGH: + case ProxyDropMode::PASSTHROUGH: ses_json.emplace("DropMode", "default"); break; - case ProxySession::DropMode::INTERCEPT: + case ProxyDropMode::INTERCEPT: ses_json.emplace("DropMode", "proxy"); break; } @@ -386,19 +386,19 @@ std::shared_ptr HTTPServer::generate_lobby_json( ret->emplace("EXPShareMultiplier", l->exp_share_multiplier); ret->emplace("AllowedDropModes", l->allowed_drop_modes); switch (l->drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: ret->emplace("DropMode", "none"); break; - case Lobby::DropMode::CLIENT: + case ServerDropMode::CLIENT: ret->emplace("DropMode", "client"); break; - case Lobby::DropMode::SERVER_SHARED: + case ServerDropMode::SERVER_SHARED: ret->emplace("DropMode", "shared"); break; - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_PRIVATE: ret->emplace("DropMode", "private"); break; - case Lobby::DropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_DUPLICATE: ret->emplace("DropMode", "duplicate"); break; } @@ -669,12 +669,23 @@ asio::awaitable> HTTPServer::generate_ep3_cards_jso }); } -asio::awaitable> HTTPServer::generate_common_tables_json() const { - auto v2_table = this->state->common_item_set_v2; - auto v3_v4_table = this->state->common_item_set_v3_v4; - co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { - return make_shared(phosg::JSON::dict({{"v1_v2", v2_table->json()}, {"v3_v4", v3_v4_table->json()}})); - }); +std::shared_ptr HTTPServer::generate_common_table_list_json() const { + auto ret = make_shared(phosg::JSON::list()); + for (const auto& it : this->state->common_item_sets) { + ret->emplace_back(it.first); + } + return ret; +} + +asio::awaitable> HTTPServer::generate_common_table_json(const std::string& table_name) const { + try { + const auto& table = this->state->common_item_sets.at(table_name); + co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr { + return make_shared(table->json()); + }); + } catch (const out_of_range&) { + throw HTTPError(404, "Table does not exist"); + } } std::shared_ptr HTTPServer::generate_rare_table_list_json() const { @@ -794,7 +805,10 @@ asio::awaitable> HTTPServer::handle_request(shared ret = co_await this->generate_ep3_cards_json(true); } else if (req.path == "/y/data/common-tables") { this->require_GET(req); - ret = co_await this->generate_common_tables_json(); + ret = this->generate_common_table_list_json(); + } else if (req.path.starts_with("/y/data/common-tables/")) { + this->require_GET(req); + ret = co_await this->generate_common_table_json(req.path.substr(22)); } else if (req.path == "/y/data/rare-tables") { this->require_GET(req); ret = this->generate_rare_table_list_json(); diff --git a/src/HTTPServer.hh b/src/HTTPServer.hh index 28f45e3a..19bd32fe 100644 --- a/src/HTTPServer.hh +++ b/src/HTTPServer.hh @@ -37,8 +37,9 @@ protected: std::shared_ptr generate_all_json() const; asio::awaitable> generate_ep3_cards_json(bool trial) const; - asio::awaitable> generate_common_tables_json() const; + std::shared_ptr generate_common_table_list_json() const; std::shared_ptr generate_rare_table_list_json() const; + asio::awaitable> generate_common_table_json(const std::string& table_name) const; asio::awaitable> generate_rare_table_json(const std::string& table_name) const; asio::awaitable> generate_quest_list_json(std::shared_ptr q); diff --git a/src/ItemParameterTable.cc b/src/ItemParameterTable.cc index c76f54de..36e03471 100644 --- a/src/ItemParameterTable.cc +++ b/src/ItemParameterTable.cc @@ -4,6 +4,41 @@ using namespace std; +template <> +ServerDropMode phosg::enum_for_name(const char* name) { + if (!strcmp(name, "DISABLED")) { + return ServerDropMode::DISABLED; + } else if (!strcmp(name, "CLIENT")) { + return ServerDropMode::CLIENT; + } else if (!strcmp(name, "SERVER_SHARED")) { + return ServerDropMode::SERVER_SHARED; + } else if (!strcmp(name, "SERVER_PRIVATE")) { + return ServerDropMode::SERVER_PRIVATE; + } else if (!strcmp(name, "SERVER_DUPLICATE")) { + return ServerDropMode::SERVER_DUPLICATE; + } else { + throw runtime_error("invalid drop mode"); + } +} + +template <> +const char* phosg::name_for_enum(ServerDropMode value) { + switch (value) { + case ServerDropMode::DISABLED: + return "DISABLED"; + case ServerDropMode::CLIENT: + return "CLIENT"; + case ServerDropMode::SERVER_SHARED: + return "SERVER_SHARED"; + case ServerDropMode::SERVER_PRIVATE: + return "SERVER_PRIVATE"; + case ServerDropMode::SERVER_DUPLICATE: + return "SERVER_DUPLICATE"; + default: + throw runtime_error("invalid drop mode"); + } +} + ItemParameterTable::ItemParameterTable(shared_ptr data, Version version) : version(version), data(data), diff --git a/src/ItemParameterTable.hh b/src/ItemParameterTable.hh index 5be2f11d..172cc0f3 100644 --- a/src/ItemParameterTable.hh +++ b/src/ItemParameterTable.hh @@ -16,6 +16,26 @@ #include "Types.hh" #include "Version.hh" +// TODO: These don't really belong here, but putting them anywhere else creates +// annoying dependency cycles. Find or make a better place for these. +enum class ServerDropMode { + DISABLED = 0, + CLIENT = 1, // Not allowed for BB games + SERVER_SHARED = 2, + SERVER_PRIVATE = 3, + SERVER_DUPLICATE = 4, +}; +enum class ProxyDropMode { + DISABLED = 0, + PASSTHROUGH, + INTERCEPT, +}; + +template <> +ServerDropMode phosg::enum_for_name(const char* name); +template <> +const char* phosg::name_for_enum(ServerDropMode value); + class ItemParameterTable { public: // TODO: This implementation is ugly. We should use real classes and virtual diff --git a/src/Lobby.cc b/src/Lobby.cc index 238d2bf6..71455b97 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -159,7 +159,7 @@ Lobby::Lobby(shared_ptr s, uint32_t id, bool is_game) challenge_exp_multiplier(1.0f), random_seed(phosg::random_object()), rand_crypt(make_shared()), - drop_mode(DropMode::CLIENT), + drop_mode(ServerDropMode::CLIENT), event(0), block(0), leader_id(0), @@ -214,41 +214,6 @@ void Lobby::create_item_creator(Version logic_version) { logic_version = leader_c ? leader_c->version() : Version::BB_V4; } - shared_ptr rare_item_set; - shared_ptr common_item_set; - switch (logic_version) { - case Version::PC_PATCH: - case Version::BB_PATCH: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - throw runtime_error("cannot create item creator for this base version"); - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - // TODO: We should probably have a v1 common item set at some point too - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v1"); - break; - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v2"); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::XB_V3: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v3"); - break; - case Version::BB_V4: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v4"); - break; - default: - throw logic_error("invalid lobby base version"); - } - shared_ptr rand_crypt; if (s->use_psov2_rand_crypt) { rand_crypt = make_shared(this->rand_crypt->seed()); @@ -256,8 +221,8 @@ void Lobby::create_item_creator(Version logic_version) { rand_crypt = make_shared(this->rand_crypt->seed()); } this->item_creator = make_shared( - common_item_set, - rare_item_set, + s->common_item_set(logic_version, this->quest), + s->rare_item_set(logic_version, this->quest), s->armor_random_set, s->tool_random_set, s->weapon_random_sets.at(this->difficulty), @@ -884,38 +849,3 @@ bool Lobby::compare_shared(const shared_ptr& a, const shared_ptrname < b->name; } - -template <> -Lobby::DropMode phosg::enum_for_name(const char* name) { - if (!strcmp(name, "DISABLED")) { - return Lobby::DropMode::DISABLED; - } else if (!strcmp(name, "CLIENT")) { - return Lobby::DropMode::CLIENT; - } else if (!strcmp(name, "SERVER_SHARED")) { - return Lobby::DropMode::SERVER_SHARED; - } else if (!strcmp(name, "SERVER_PRIVATE")) { - return Lobby::DropMode::SERVER_PRIVATE; - } else if (!strcmp(name, "SERVER_DUPLICATE")) { - return Lobby::DropMode::SERVER_DUPLICATE; - } else { - throw runtime_error("invalid drop mode"); - } -} - -template <> -const char* phosg::name_for_enum(Lobby::DropMode value) { - switch (value) { - case Lobby::DropMode::DISABLED: - return "DISABLED"; - case Lobby::DropMode::CLIENT: - return "CLIENT"; - case Lobby::DropMode::SERVER_SHARED: - return "SERVER_SHARED"; - case Lobby::DropMode::SERVER_PRIVATE: - return "SERVER_PRIVATE"; - case Lobby::DropMode::SERVER_DUPLICATE: - return "SERVER_DUPLICATE"; - default: - throw runtime_error("invalid drop mode"); - } -} diff --git a/src/Lobby.hh b/src/Lobby.hh index 6a299ba4..3dd6928f 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -90,13 +90,6 @@ struct Lobby : public std::enable_shared_from_this { IS_OVERFLOW = 0x08000000, // clang-format on }; - enum class DropMode { - DISABLED = 0, - CLIENT = 1, // Not allowed for BB games - SERVER_SHARED = 2, - SERVER_PRIVATE = 3, - SERVER_DUPLICATE = 4, - }; std::weak_ptr server_state; phosg::PrefixedLogger log; @@ -136,7 +129,7 @@ struct Lobby : public std::enable_shared_from_this { uint32_t random_seed; std::shared_ptr rand_crypt; uint8_t allowed_drop_modes; - DropMode drop_mode; + ServerDropMode drop_mode; std::shared_ptr item_creator; // Always null for lobbies, never null for games struct ChallengeParameters { @@ -291,6 +284,6 @@ struct Lobby : public std::enable_shared_from_this { }; template <> -Lobby::DropMode phosg::enum_for_name(const char* name); +ServerDropMode phosg::enum_for_name(const char* name); template <> -const char* phosg::name_for_enum(Lobby::DropMode value); +const char* phosg::name_for_enum(ServerDropMode value); diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 363e359f..ea78c951 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -844,13 +844,12 @@ static asio::awaitable SC_6x60_6xA2(shared_ptr c, Channel co_return HandlerResult::FORWARD; } - using DropMode = ProxySession::DropMode; switch (c->proxy_session->drop_mode) { - case DropMode::DISABLED: + case ProxyDropMode::DISABLED: co_return HandlerResult::SUPPRESS; - case DropMode::PASSTHROUGH: + case ProxyDropMode::PASSTHROUGH: co_return HandlerResult::FORWARD; - case DropMode::INTERCEPT: + case ProxyDropMode::INTERCEPT: break; default: throw logic_error("invalid drop mode"); diff --git a/src/ProxySession.cc b/src/ProxySession.cc index 74470fd5..4d7a8a2a 100644 --- a/src/ProxySession.cc +++ b/src/ProxySession.cc @@ -21,47 +21,14 @@ ProxySession::~ProxySession() { this->num_proxy_sessions--; } -void ProxySession::set_drop_mode(shared_ptr s, Version version, int64_t override_random_seed, DropMode new_mode) { +void ProxySession::set_drop_mode( + shared_ptr s, Version version, int64_t override_random_seed, ProxyDropMode new_mode) { this->drop_mode = new_mode; - if (this->drop_mode == DropMode::INTERCEPT) { - shared_ptr rare_item_set; - shared_ptr common_item_set; - switch (version) { - case Version::PC_PATCH: - case Version::BB_PATCH: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - throw runtime_error("cannot create item creator for this base version"); - case Version::DC_NTE: - case Version::DC_11_2000: - case Version::DC_V1: - // TODO: We should probably have a v1 common item set at some point too - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v1"); - break; - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - common_item_set = s->common_item_set_v2; - rare_item_set = s->rare_item_sets.at("rare-table-v2"); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::XB_V3: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v3"); - break; - case Version::BB_V4: - common_item_set = s->common_item_set_v3_v4; - rare_item_set = s->rare_item_sets.at("rare-table-v4"); - break; - default: - throw logic_error("invalid lobby base version"); - } + if (this->drop_mode == ProxyDropMode::INTERCEPT) { auto rand_crypt = make_shared((override_random_seed >= 0) ? override_random_seed : this->lobby_random_seed); this->item_creator = make_shared( - common_item_set, - rare_item_set, + s->common_item_set(version, nullptr), + s->rare_item_set(version, nullptr), s->armor_random_set, s->tool_random_set, s->weapon_random_sets.at(this->lobby_difficulty), diff --git a/src/ProxySession.hh b/src/ProxySession.hh index 0e003393..af9e2980 100644 --- a/src/ProxySession.hh +++ b/src/ProxySession.hh @@ -47,12 +47,7 @@ struct ProxySession { int64_t remote_guild_card_number = -1; parray remote_client_config_data; - enum class DropMode { - DISABLED = 0, - PASSTHROUGH, - INTERCEPT, - }; - DropMode drop_mode = DropMode::PASSTHROUGH; + ProxyDropMode drop_mode = ProxyDropMode::PASSTHROUGH; std::shared_ptr quest_dat_data; std::shared_ptr item_creator; std::shared_ptr map_state; @@ -80,7 +75,7 @@ struct ProxySession { }; std::unordered_map saving_files; - void set_drop_mode(std::shared_ptr s, Version version, int64_t override_random_seed, DropMode new_mode); + void set_drop_mode(std::shared_ptr s, Version version, int64_t override_random_seed, ProxyDropMode new_mode); void clear_lobby_players(size_t num_slots); }; diff --git a/src/Quest.cc b/src/Quest.cc index 85699f6f..874dffb6 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -221,6 +221,15 @@ void VersionedQuest::assert_valid() const { if (!is_ep3(this->version) && !this->map_file) { throw runtime_error("parsed map file is missing"); } + if (this->common_item_set_name.empty() != !this->common_item_set) { + throw runtime_error("common item set name/pointer mismatch"); + } + if (this->rare_item_set_name.empty() != !this->rare_item_set) { + throw runtime_error("rare item set name/pointer mismatch"); + } + if (this->allowed_drop_modes && !(this->allowed_drop_modes & (1 << static_cast(this->default_drop_mode)))) { + throw runtime_error("default drop mode is not allowed"); + } } string VersionedQuest::bin_filename() const { @@ -280,7 +289,13 @@ Quest::Quest(shared_ptr initial_version) challenge_template_index(initial_version->challenge_template_index), description_flag(initial_version->description_flag), available_expression(initial_version->available_expression), - enabled_expression(initial_version->enabled_expression) { + enabled_expression(initial_version->enabled_expression), + common_item_set_name(initial_version->common_item_set_name), + rare_item_set_name(initial_version->rare_item_set_name), + common_item_set(initial_version->common_item_set), + rare_item_set(initial_version->rare_item_set), + allowed_drop_modes(initial_version->allowed_drop_modes), + default_drop_mode(initial_version->default_drop_mode) { this->add_version(initial_version); } @@ -298,10 +313,6 @@ phosg::JSON Quest::json() const { })); } - auto battle_rules_json = this->battle_rules ? this->battle_rules->json() : nullptr; - auto challenge_template_index_json = (this->challenge_template_index >= 0) - ? this->challenge_template_index - : phosg::JSON(nullptr); return phosg::JSON::dict({ {"Number", this->quest_number}, {"CategoryID", this->category_id}, @@ -311,11 +322,15 @@ phosg::JSON Quest::json() const { {"MaxPlayers", this->max_players}, {"LockStatusRegister", (this->lock_status_register >= 0) ? this->lock_status_register : phosg::JSON(nullptr)}, {"Name", this->name}, - {"BattleRules", std::move(battle_rules_json)}, - {"ChallengeTemplateIndex", std::move(challenge_template_index_json)}, + {"BattleRules", this->battle_rules ? this->battle_rules->json() : phosg::JSON(nullptr)}, + {"ChallengeTemplateIndex", (this->challenge_template_index >= 0) ? this->challenge_template_index : phosg::JSON(nullptr)}, {"DescriptionFlag", this->description_flag}, {"AvailableExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, {"EnabledExpression", this->available_expression ? this->available_expression->str() : phosg::JSON(nullptr)}, + {"CommonItemSetName", this->common_item_set_name.empty() ? phosg::JSON(nullptr) : this->common_item_set_name}, + {"RareItemSetName", this->rare_item_set_name.empty() ? phosg::JSON(nullptr) : this->rare_item_set_name}, + {"AllowedDropModes", this->allowed_drop_modes}, + {"DefaultDropMode", phosg::name_for_enum(this->default_drop_mode)}, {"Versions", std::move(versions_json)}, }); } @@ -406,6 +421,30 @@ void Quest::add_version(shared_ptr vq) { "quest version has a different enabled expression (existing: {}, new: {})", existing_str, new_str)); } + if (this->common_item_set_name != vq->common_item_set_name) { + throw runtime_error(std::format( + "quest version has different common table name (existing: {}, new: {})", + this->common_item_set_name, vq->common_item_set_name)); + } + if (this->common_item_set != vq->common_item_set) { + throw runtime_error("quest version has different common table"); + } + if (this->rare_item_set_name != vq->rare_item_set_name) { + throw runtime_error(std::format( + "quest version has different rare table name (existing: {}, new: {})", + this->rare_item_set_name, vq->rare_item_set_name)); + } + if (this->rare_item_set != vq->rare_item_set) { + throw runtime_error("quest version has different rare table"); + } + if (this->allowed_drop_modes != vq->allowed_drop_modes) { + throw runtime_error(format("quest version has different allowed drop modes (existing: {:02X}, new: {:02X})", + this->allowed_drop_modes, vq->allowed_drop_modes)); + } + if (this->default_drop_mode != vq->default_drop_mode) { + throw runtime_error(format("quest version has different default drop mode (existing: {}, new: {})", + phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(vq->default_drop_mode))); + } this->versions.emplace(this->versions_key(vq->version, vq->language), vq); } @@ -482,6 +521,8 @@ shared_ptr Quest::version(Version v, uint8_t language) con QuestIndex::QuestIndex( const string& directory, shared_ptr category_index, + const unordered_map>& common_item_sets, + const unordered_map>& rare_item_sets, bool is_ep3) : directory(directory), category_index(category_index) { @@ -914,6 +955,28 @@ QuestIndex::QuestIndex( vq->lock_status_register = metadata_json.get_int("LockStatusRegister"); } catch (const out_of_range&) { } + try { + vq->common_item_set_name = metadata_json.at("CommonItemSetName").as_string(); + } catch (const out_of_range&) { + } + if (!vq->common_item_set_name.empty()) { + vq->common_item_set = common_item_sets.at(vq->common_item_set_name); + } + try { + vq->rare_item_set_name = metadata_json.at("RareItemSetName").as_string(); + } catch (const out_of_range&) { + } + if (!vq->rare_item_set_name.empty()) { + vq->rare_item_set = rare_item_sets.at(vq->rare_item_set_name); + } + try { + vq->allowed_drop_modes = metadata_json.at("AllowedDropModes").as_int(); + } catch (const out_of_range&) { + } + try { + vq->default_drop_mode = phosg::enum_for_name(metadata_json.at("DefaultDropMode").as_string()); + } catch (const out_of_range&) { + } } vq->assert_valid(); diff --git a/src/Quest.hh b/src/Quest.hh index 1f94bdaa..52a19c32 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -8,10 +8,13 @@ #include #include +#include "CommonItemSet.hh" #include "IntegralExpression.hh" +#include "ItemParameterTable.hh" #include "Map.hh" #include "PlayerSubordinates.hh" #include "QuestScript.hh" +#include "RareItemSet.hh" #include "StaticGameData.hh" #include "TeamIndex.hh" @@ -87,6 +90,12 @@ struct VersionedQuest { std::shared_ptr enabled_expression; bool allow_start_from_chat_command = false; int16_t lock_status_register = -1; + std::string common_item_set_name; + std::string rare_item_set_name; + std::shared_ptr common_item_set; + std::shared_ptr rare_item_set; + uint8_t allowed_drop_modes = 0x00; // 0 = use server default + ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 bool is_dlq_encoded = false; void assert_valid() const; @@ -115,6 +124,13 @@ struct Quest { uint8_t description_flag; std::shared_ptr available_expression; std::shared_ptr enabled_expression; + std::string common_item_set_name; + std::string rare_item_set_name; + std::shared_ptr common_item_set; + std::shared_ptr rare_item_set; + uint8_t allowed_drop_modes = 0x00; // 0 = use server default + ServerDropMode default_drop_mode = ServerDropMode::CLIENT; // Ignored if allowed_drop_modes == 0 + std::map> versions; Quest() = delete; @@ -151,7 +167,12 @@ struct QuestIndex { std::map> quests_by_name; std::map>> quests_by_category_id_and_number; - QuestIndex(const std::string& directory, std::shared_ptr category_index, bool is_ep3); + QuestIndex( + const std::string& directory, + std::shared_ptr category_index, + const std::unordered_map>& common_item_sets, + const std::unordered_map>& rare_item_sets, + bool is_ep3); phosg::JSON json() const; std::shared_ptr get(uint32_t quest_number) const; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index f9c8a842..ae309f25 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -2252,19 +2252,19 @@ static asio::awaitable on_09(shared_ptr c, Channel::Message& msg) } switch (game->drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: info += "$C6Drops disabled$C7\n"; break; - case Lobby::DropMode::CLIENT: + case ServerDropMode::CLIENT: info += "$C6Client drops$C7\n"; break; - case Lobby::DropMode::SERVER_SHARED: + case ServerDropMode::SERVER_SHARED: info += "$C6Server drops$C7\n"; break; - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_PRIVATE: info += "$C6Private drops$C7\n"; break; - case Lobby::DropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_DUPLICATE: info += "$C6Duplicate drops$C7\n"; break; } @@ -2433,6 +2433,10 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi if (l->episode != Episode::EP3) { l->episode = q->episode; } + if (l->quest->allowed_drop_modes) { + l->allowed_drop_modes = l->quest->allowed_drop_modes; + l->drop_mode = l->quest->default_drop_mode; + } l->create_item_creator(); size_t num_clients_with_loading_flag = 0; @@ -4585,7 +4589,7 @@ shared_ptr create_game_generic( case Version::GC_EP3_NTE: case Version::GC_EP3: quest_flag_rewrites = nullptr; - game->drop_mode = Lobby::DropMode::DISABLED; + game->drop_mode = ServerDropMode::DISABLED; game->allowed_drop_modes = (1 << static_cast(game->drop_mode)); break; case Version::BB_V4: @@ -4601,10 +4605,10 @@ shared_ptr create_game_generic( game->allowed_drop_modes = s->allowed_drop_modes_v4_normal; } // Disallow CLIENT mode on BB - if (game->drop_mode == Lobby::DropMode::CLIENT) { + if (game->drop_mode == ServerDropMode::CLIENT) { throw logic_error("CLIENT mode not allowed on BB"); } - if (game->allowed_drop_modes & (1 << static_cast(Lobby::DropMode::CLIENT))) { + if (game->allowed_drop_modes & (1 << static_cast(ServerDropMode::CLIENT))) { throw logic_error("CLIENT mode not allowed on BB"); } break; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 6c919b13..96b74766 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -2186,7 +2186,7 @@ static void on_box_or_enemy_item_drop_t(shared_ptr c, SubcommandMessage& throw runtime_error("BB client sent 6x5F command"); } - bool should_notify = s->rare_notifs_enabled_for_client_drops && (l->drop_mode == Lobby::DropMode::CLIENT); + bool should_notify = s->rare_notifs_enabled_for_client_drops && (l->drop_mode == ServerDropMode::CLIENT); shared_ptr ene_st; shared_ptr obj_st; @@ -2956,27 +2956,27 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S G_SpecializableItemDropRequest_6xA2 cmd = normalize_drop_request(msg.data, msg.size); auto rec = reconcile_drop_request_with_map(c, cmd, l->episode, l->event, l->map_state, true); - Lobby::DropMode drop_mode = l->drop_mode; + ServerDropMode drop_mode = l->drop_mode; switch (drop_mode) { - case Lobby::DropMode::DISABLED: + case ServerDropMode::DISABLED: co_return; - case Lobby::DropMode::CLIENT: { + case ServerDropMode::CLIENT: { // If the leader is BB, use SERVER_SHARED instead // TODO: We should also use server drops if any clients have incompatible // object lists, since they might generate incorrect IDs for items and we // can't override them auto leader = l->clients[l->leader_id]; if (leader && leader->version() == Version::BB_V4) { - drop_mode = Lobby::DropMode::SERVER_SHARED; + drop_mode = ServerDropMode::SERVER_SHARED; break; } else { forward_subcommand(c, msg); co_return; } } - case Lobby::DropMode::SERVER_SHARED: - case Lobby::DropMode::SERVER_DUPLICATE: - case Lobby::DropMode::SERVER_PRIVATE: + case ServerDropMode::SERVER_SHARED: + case ServerDropMode::SERVER_DUPLICATE: + case ServerDropMode::SERVER_PRIVATE: break; default: throw logic_error("invalid drop mode"); @@ -3016,11 +3016,11 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S }; switch (drop_mode) { - case Lobby::DropMode::DISABLED: - case Lobby::DropMode::CLIENT: + case ServerDropMode::DISABLED: + case ServerDropMode::CLIENT: throw logic_error("unhandled simple drop mode"); - case Lobby::DropMode::SERVER_SHARED: - case Lobby::DropMode::SERVER_DUPLICATE: { + case ServerDropMode::SERVER_SHARED: + case ServerDropMode::SERVER_DUPLICATE: { // TODO: In SERVER_DUPLICATE mode, should we reduce the rates for rare // items? Maybe by a factor of l->count_clients()? auto res = generate_item(); @@ -3029,7 +3029,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S } else { string name = s->describe_item(c->version(), res.item); l->log.info_f("Entity {:04X} (area {:02X}) created item {}", cmd.entity_index, cmd.effective_area, name); - if (drop_mode == Lobby::DropMode::SERVER_DUPLICATE) { + if (drop_mode == ServerDropMode::SERVER_DUPLICATE) { for (const auto& lc : l->clients) { if (lc && (rec.obj_st || (lc->floor == cmd.floor))) { res.item.id = l->generate_item_id(0xFF); @@ -3058,7 +3058,7 @@ static asio::awaitable on_entity_drop_item_request(shared_ptr c, S } break; } - case Lobby::DropMode::SERVER_PRIVATE: { + case ServerDropMode::SERVER_PRIVATE: { for (const auto& lc : l->clients) { if (lc && (rec.obj_st || (lc->floor == cmd.floor))) { auto res = generate_item(); @@ -3135,7 +3135,7 @@ static asio::awaitable on_set_quest_flag(shared_ptr c, SubcommandM forward_subcommand(c, msg); - if (l->drop_mode != Lobby::DropMode::DISABLED) { + if (l->drop_mode != ServerDropMode::DISABLED) { EnemyType boss_enemy_type = EnemyType::NONE; bool is_ep2 = (l->episode == Episode::EP2); if ((l->episode == Episode::EP1) && (c->floor == 0x0E)) { @@ -4916,7 +4916,7 @@ static asio::awaitable on_photon_crystal_exchange_bb(shared_ptr c, size_t index = p->inventory.find_item_by_primary_identifier(0x03100200); auto item = p->remove_item(p->inventory.items[index].data.id, 1, *s->item_stack_limits(c->version())); send_destroy_item_to_lobby(c, item.id, 1); - l->drop_mode = Lobby::DropMode::DISABLED; + l->drop_mode = ServerDropMode::DISABLED; l->allowed_drop_modes = (1 << static_cast(l->drop_mode)); // DISABLED only co_return; } diff --git a/src/ServerState.cc b/src/ServerState.cc index b21c7c18..825055a2 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -512,6 +512,35 @@ ItemData ServerState::parse_item_description(Version version, const string& desc return this->item_name_index(version)->parse_item_description(description); } +shared_ptr ServerState::common_item_set(Version logic_version, shared_ptr q) const { + if (q && q->common_item_set) { + return q->common_item_set; + } else if (is_v1_or_v2(logic_version)) { + // TODO: We should probably have a v1 common item set at some point too + return this->common_item_sets.at("common-table-v1-v2"); + } else if (is_v3(logic_version) || is_v4(logic_version)) { + return this->common_item_sets.at("common-table-v3-v4"); + } else { + throw runtime_error(std::format("no default common item set is available for {}", phosg::name_for_enum(logic_version))); + } +} + +shared_ptr ServerState::rare_item_set(Version logic_version, shared_ptr q) const { + if (q && q->rare_item_set) { + return q->rare_item_set; + } else if (is_v1(logic_version)) { + return this->rare_item_sets.at("rare-table-v1"); + } else if (is_v2(logic_version)) { + return this->rare_item_sets.at("rare-table-v2"); + } else if (is_v3(logic_version)) { + return this->rare_item_sets.at("rare-table-v3"); + } else if (is_v4(logic_version)) { + return this->rare_item_sets.at("rare-table-v4"); + } else { + throw runtime_error(std::format("no default rare item set is available for {}", phosg::name_for_enum(logic_version))); + } +} + void ServerState::set_port_configuration(const vector& port_configs) { this->name_to_port_config.clear(); this->number_to_port_config.clear(); @@ -835,22 +864,22 @@ void ServerState::load_config_early() { this->allowed_drop_modes_v4_normal = this->config_json->get_int("AllowedDropModesV4Normal", 0x1D); this->allowed_drop_modes_v4_battle = this->config_json->get_int("AllowedDropModesV4Battle", 0x05); this->allowed_drop_modes_v4_challenge = this->config_json->get_int("AllowedDropModesV4Challenge", 0x05); - this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", Lobby::DropMode::CLIENT); - this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", Lobby::DropMode::CLIENT); - this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", Lobby::DropMode::CLIENT); - this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", Lobby::DropMode::CLIENT); - this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", Lobby::DropMode::SERVER_SHARED); - this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", Lobby::DropMode::SERVER_SHARED); - this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", Lobby::DropMode::SERVER_SHARED); - if ((this->default_drop_mode_v4_normal == Lobby::DropMode::CLIENT) || - (this->default_drop_mode_v4_battle == Lobby::DropMode::CLIENT) || - (this->default_drop_mode_v4_challenge == Lobby::DropMode::CLIENT)) { + this->default_drop_mode_v1_v2_normal = this->config_json->get_enum("DefaultDropModeV1V2Normal", ServerDropMode::CLIENT); + this->default_drop_mode_v1_v2_battle = this->config_json->get_enum("DefaultDropModeV1V2Battle", ServerDropMode::CLIENT); + this->default_drop_mode_v1_v2_challenge = this->config_json->get_enum("DefaultDropModeV1V2Challenge", ServerDropMode::CLIENT); + this->default_drop_mode_v3_normal = this->config_json->get_enum("DefaultDropModeV3Normal", ServerDropMode::CLIENT); + this->default_drop_mode_v3_battle = this->config_json->get_enum("DefaultDropModeV3Battle", ServerDropMode::CLIENT); + this->default_drop_mode_v3_challenge = this->config_json->get_enum("DefaultDropModeV3Challenge", ServerDropMode::CLIENT); + this->default_drop_mode_v4_normal = this->config_json->get_enum("DefaultDropModeV4Normal", ServerDropMode::SERVER_SHARED); + this->default_drop_mode_v4_battle = this->config_json->get_enum("DefaultDropModeV4Battle", ServerDropMode::SERVER_SHARED); + this->default_drop_mode_v4_challenge = this->config_json->get_enum("DefaultDropModeV4Challenge", ServerDropMode::SERVER_SHARED); + if ((this->default_drop_mode_v4_normal == ServerDropMode::CLIENT) || + (this->default_drop_mode_v4_battle == ServerDropMode::CLIENT) || + (this->default_drop_mode_v4_challenge == ServerDropMode::CLIENT)) { throw runtime_error("default V4 drop mode cannot be CLIENT"); } - if ((this->allowed_drop_modes_v4_normal & (1 << static_cast(Lobby::DropMode::CLIENT))) || - (this->allowed_drop_modes_v4_battle & (1 << static_cast(Lobby::DropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast(Lobby::DropMode::CLIENT)))) { + if ((this->allowed_drop_modes_v4_normal & (1 << static_cast(ServerDropMode::CLIENT))) || + (this->allowed_drop_modes_v4_battle & (1 << static_cast(ServerDropMode::CLIENT))) || (this->allowed_drop_modes_v4_challenge & (1 << static_cast(ServerDropMode::CLIENT)))) { throw runtime_error("CLIENT drop mode cannot be allowed in V4"); } @@ -1940,61 +1969,103 @@ void ServerState::load_item_name_indexes() { } void ServerState::load_drop_tables() { - config_log.info_f("Loading rare item sets"); + config_log.info_f("Loading item sets"); - unordered_map> new_rare_item_sets; + unordered_map> new_rare_item_sets; + unordered_map> new_common_item_sets; for (const auto& item : std::filesystem::directory_iterator("system/item-tables")) { string filename = item.path().filename().string(); - if (!filename.starts_with("rare-table-")) { - continue; - } - string path = "system/item-tables/" + filename; - size_t ext_offset = filename.rfind('.'); - string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset); + if (filename.starts_with("common-table-") || filename.starts_with("ItemPT-")) { + string path = "system/item-tables/" + filename; + size_t ext_offset = filename.rfind('.'); + string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset); - if (filename.ends_with("-v1.json")) { - config_log.info_f("Loading v1 JSON rare item table {}", filename); - new_rare_item_sets.emplace(basename, make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1))); - } else if (filename.ends_with("-v2.json")) { - config_log.info_f("Loading v2 JSON rare item table {}", filename); - new_rare_item_sets.emplace(basename, make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2))); - } else if (filename.ends_with("-v3.json")) { - config_log.info_f("Loading v3 JSON rare item table {}", filename); - new_rare_item_sets.emplace(basename, make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3))); - } else if (filename.ends_with("-v4.json")) { - config_log.info_f("Loading v4 JSON rare item table {}", filename); - new_rare_item_sets.emplace(basename, make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4))); + // AFSV2CommonItemSet(std::shared_ptr pt_afs_data, std::shared_ptr ct_afs_data); - } else if (filename.ends_with(".afs")) { - config_log.info_f("Loading AFS rare item table {}", filename); - auto data = make_shared(phosg::load_file(path)); - new_rare_item_sets.emplace(basename, make_shared(AFSArchive(data), false)); + if (filename.ends_with(".json")) { + config_log.info_f("Loading JSON common item table {}", filename); + new_common_item_sets.emplace(basename, make_shared(phosg::JSON::parse(phosg::load_file(path)))); + } else if (filename.ends_with(".afs")) { + string ct_filename; + if (filename.starts_with("ItemPT-")) { + ct_filename = "ItemCT-" + filename.substr(7); + } else if (filename.starts_with("common-table-")) { + ct_filename = "challenge-common-table-" + filename.substr(13); + } else { + throw std::runtime_error(std::format("cannot determine challenge table filename for common table file: {}", filename)); + } + auto data = make_shared(phosg::load_file(path)); + shared_ptr ct_data; + try { + string ct_path = "system/item-tables/" + ct_filename; + ct_data = make_shared(phosg::load_file(ct_path)); + config_log.info_f("Loading AFS common item table {} with challenge table {}", filename, ct_filename); + } catch (const phosg::cannot_open_file&) { + config_log.info_f("Loading AFS common item table {} without challenge table", filename); + } + new_common_item_sets.emplace(basename, make_shared(data, ct_data)); + } else if (filename.ends_with(".gsl")) { + config_log.info_f("Loading little-endian GSL common item table {}", filename); + auto data = make_shared(phosg::load_file(path)); + new_common_item_sets.emplace(basename, make_shared(data, false)); + } else if (filename.ends_with(".gslb")) { + config_log.info_f("Loading big-endian GSL common item table {}", filename); + auto data = make_shared(phosg::load_file(path)); + new_common_item_sets.emplace(basename, make_shared(data, true)); + } else { + throw std::runtime_error(std::format("unknown format for common table file: {}", filename)); + } - } else if (filename.ends_with(".gsl")) { - config_log.info_f("Loading GSL rare item table {}", filename); - auto data = make_shared(phosg::load_file(path)); - new_rare_item_sets.emplace(basename, make_shared(GSLArchive(data, false), false)); + } else if (filename.starts_with("rare-table-") || filename.starts_with("ItemRT-")) { + string path = "system/item-tables/" + filename; + size_t ext_offset = filename.rfind('.'); + string basename = (ext_offset == string::npos) ? filename : filename.substr(0, ext_offset); - } else if (filename.ends_with(".gslb")) { - config_log.info_f("Loading GSL rare item table {}", filename); - auto data = make_shared(phosg::load_file(path)); - new_rare_item_sets.emplace(basename, make_shared(GSLArchive(data, true), true)); + shared_ptr rare_set; + if (filename.ends_with("-v1.json")) { + config_log.info_f("Loading v1 JSON rare item table {}", filename); + rare_set = make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::DC_V1)); + } else if (filename.ends_with("-v2.json")) { + config_log.info_f("Loading v2 JSON rare item table {}", filename); + rare_set = make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::PC_V2)); + } else if (filename.ends_with("-v3.json")) { + config_log.info_f("Loading v3 JSON rare item table {}", filename); + rare_set = make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::GC_V3)); + } else if (filename.ends_with("-v4.json")) { + config_log.info_f("Loading v4 JSON rare item table {}", filename); + rare_set = make_shared(phosg::JSON::parse(phosg::load_file(path)), this->item_name_index(Version::BB_V4)); - } else if (filename.ends_with(".rel")) { - config_log.info_f("Loading REL rare item table {}", filename); - new_rare_item_sets.emplace(basename, make_shared(phosg::load_file(path), true)); + } else if (filename.ends_with(".afs")) { + config_log.info_f("Loading AFS rare item table {}", filename); + auto data = make_shared(phosg::load_file(path)); + rare_set = make_shared(AFSArchive(data), false); + + } else if (filename.ends_with(".gsl")) { + config_log.info_f("Loading GSL rare item table {}", filename); + auto data = make_shared(phosg::load_file(path)); + rare_set = make_shared(GSLArchive(data, false), false); + + } else if (filename.ends_with(".gslb")) { + config_log.info_f("Loading GSL rare item table {}", filename); + auto data = make_shared(phosg::load_file(path)); + rare_set = make_shared(GSLArchive(data, true), true); + + } else if (filename.ends_with(".rel")) { + config_log.info_f("Loading REL rare item table {}", filename); + rare_set = make_shared(phosg::load_file(path), true); + + } else { + throw std::runtime_error(std::format("unknown format for rare table file: {}", filename)); + } + + if (this->server_global_drop_rate_multiplier != 1.0) { + rare_set->multiply_all_rates(this->server_global_drop_rate_multiplier); + } + new_rare_item_sets.emplace(basename, std::move(rare_set)); } } - config_log.info_f("Loading v2 common item table"); - auto ct_data_v2 = make_shared(phosg::load_file("system/item-tables/ItemCT-pc-v2.afs")); - auto pt_data_v2 = make_shared(phosg::load_file("system/item-tables/ItemPT-pc-v2.afs")); - auto new_common_item_set_v2 = make_shared(pt_data_v2, ct_data_v2); - config_log.info_f("Loading v3+v4 common item table"); - auto pt_data_v3_v4 = make_shared(phosg::load_file("system/item-tables/ItemPT-gc-v3.gsl")); - auto new_common_item_set_v3_v4 = make_shared(pt_data_v3_v4, true); - config_log.info_f("Loading armor table"); auto armor_data = make_shared(phosg::load_file("system/item-tables/ArmorRandom-gc-v3.rel")); auto new_armor_random_set = make_shared(armor_data); @@ -2020,19 +2091,8 @@ void ServerState::load_drop_tables() { auto tekker_data = make_shared(phosg::load_file("system/item-tables/JudgeItem-gc-v3.rel")); auto new_tekker_adjustment_set = make_shared(tekker_data); - if (this->server_global_drop_rate_multiplier != 1.0) { - for (auto& it : new_rare_item_sets) { - it.second->multiply_all_rates(this->server_global_drop_rate_multiplier); - } - } - // We can't just std::move() new_rare_item_sets into place because its values are - // not const :( - this->rare_item_sets.clear(); - for (auto& it : new_rare_item_sets) { - this->rare_item_sets.emplace(it.first, std::move(it.second)); - } - this->common_item_set_v2 = std::move(new_common_item_set_v2); - this->common_item_set_v3_v4 = std::move(new_common_item_set_v3_v4); + this->rare_item_sets = std::move(new_rare_item_sets); + this->common_item_sets = std::move(new_common_item_sets); this->armor_random_set = std::move(new_armor_random_set); this->tool_random_set = std::move(new_tool_random_set); this->weapon_random_sets = std::move(new_weapon_random_sets); @@ -2107,9 +2167,14 @@ void ServerState::load_ep3_tournament_state() { void ServerState::load_quest_index() { config_log.info_f("Collecting quests"); - this->default_quest_index = make_shared("system/quests", this->quest_category_index, false); + this->default_quest_index = make_shared("system/quests", this->quest_category_index, this->common_item_sets, this->rare_item_sets, false); config_log.info_f("Collecting Episode 3 download quests"); - this->ep3_download_quest_index = make_shared("system/ep3/maps-download", this->quest_category_index, true); + this->ep3_download_quest_index = make_shared( + "system/ep3/maps-download", + this->quest_category_index, + unordered_map>{}, + unordered_map>{}, + true); } void ServerState::compile_functions() { diff --git a/src/ServerState.hh b/src/ServerState.hh index 29b16693..d0a29bda 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -128,15 +128,15 @@ struct ServerState : public std::enable_shared_from_this { uint8_t allowed_drop_modes_v4_normal = 0x1D; // CLIENT not allowed uint8_t allowed_drop_modes_v4_battle = 0x05; uint8_t allowed_drop_modes_v4_challenge = 0x05; - Lobby::DropMode default_drop_mode_v1_v2_normal = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v1_v2_battle = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v1_v2_challenge = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v3_normal = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v3_battle = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v3_challenge = Lobby::DropMode::CLIENT; - Lobby::DropMode default_drop_mode_v4_normal = Lobby::DropMode::SERVER_SHARED; - Lobby::DropMode default_drop_mode_v4_battle = Lobby::DropMode::SERVER_SHARED; - Lobby::DropMode default_drop_mode_v4_challenge = Lobby::DropMode::SERVER_SHARED; + ServerDropMode default_drop_mode_v1_v2_normal = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v1_v2_battle = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v1_v2_challenge = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_normal = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_battle = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v3_challenge = ServerDropMode::CLIENT; + ServerDropMode default_drop_mode_v4_normal = ServerDropMode::SERVER_SHARED; + ServerDropMode default_drop_mode_v4_battle = ServerDropMode::SERVER_SHARED; + ServerDropMode default_drop_mode_v4_challenge = ServerDropMode::SERVER_SHARED; std::unordered_map quest_flag_rewrites_v1_v2; std::unordered_map quest_flag_rewrites_v3; std::unordered_map quest_flag_rewrites_v4; @@ -196,9 +196,8 @@ struct ServerState : public std::enable_shared_from_this { std::shared_ptr level_table_v4; std::shared_ptr battle_params; std::shared_ptr bb_data_gsl; + std::unordered_map> common_item_sets; std::unordered_map> rare_item_sets; - std::shared_ptr common_item_set_v2; - std::shared_ptr common_item_set_v3_v4; std::shared_ptr armor_random_set; std::shared_ptr tool_random_set; std::array, 4> weapon_random_sets; @@ -357,6 +356,9 @@ struct ServerState : public std::enable_shared_from_this { std::string describe_item(Version version, const ItemData& item, uint8_t flags = 0) const; ItemData parse_item_description(Version version, const std::string& description) const; + std::shared_ptr common_item_set(Version logic_version, std::shared_ptr q) const; + std::shared_ptr rare_item_set(Version logic_version, std::shared_ptr q) const; + const std::vector& public_lobby_search_order(Version version, bool is_client_customization) const; inline const std::vector& public_lobby_search_order(std::shared_ptr c) const { return this->public_lobby_search_order(c->version(), c->check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)); diff --git a/system/item-tables/ItemPT-gc-nte.gsl b/system/item-tables/ItemPT-gc-nte.gslb similarity index 100% rename from system/item-tables/ItemPT-gc-nte.gsl rename to system/item-tables/ItemPT-gc-nte.gslb diff --git a/system/item-tables/ItemPT-gc-v3.gsl b/system/item-tables/ItemPT-gc-v3.gslb similarity index 100% rename from system/item-tables/ItemPT-gc-v3.gsl rename to system/item-tables/ItemPT-gc-v3.gslb diff --git a/system/item-tables/ItemRT-gc-nte.gsl b/system/item-tables/ItemRT-gc-nte.gslb similarity index 100% rename from system/item-tables/ItemRT-gc-nte.gsl rename to system/item-tables/ItemRT-gc-nte.gslb diff --git a/system/item-tables/challenge-common-table-v1-v2.afs b/system/item-tables/challenge-common-table-v1-v2.afs new file mode 120000 index 00000000..e8a6fc3e --- /dev/null +++ b/system/item-tables/challenge-common-table-v1-v2.afs @@ -0,0 +1 @@ +ItemCT-pc-v2.afs \ No newline at end of file diff --git a/system/item-tables/common-table-v1-v2.afs b/system/item-tables/common-table-v1-v2.afs new file mode 120000 index 00000000..a021351d --- /dev/null +++ b/system/item-tables/common-table-v1-v2.afs @@ -0,0 +1 @@ +ItemPT-pc-v2.afs \ No newline at end of file diff --git a/system/item-tables/common-table-v3-v4.gslb b/system/item-tables/common-table-v3-v4.gslb new file mode 120000 index 00000000..5ac304e8 --- /dev/null +++ b/system/item-tables/common-table-v3-v4.gslb @@ -0,0 +1 @@ +ItemPT-gc-v3.gslb \ No newline at end of file diff --git a/system/quests/battle/b88001.json b/system/quests/battle/b88001.json index a16ccf29..c6988120 100644 --- a/system/quests/battle/b88001.json +++ b/system/quests/battle/b88001.json @@ -75,4 +75,29 @@ // via $quest, AvailableIf and EnabledIf are not checked, so it's inadvisable // to use this option if either of those options are also used. // "AllowStartFromChatCommand": true, + + // If this field is specified, it overrides the default common item table for + // the duration of the quest. The common item table name must begin with + // either "common-table-" or "ItemPT-", and the corresponding file should be + // in system/item-tables/ and should be in .json, .afs, .gsl, or .gslb + // format. The file extension (.json, etc.) should not be included here. If + // you use this, make sure to set AllowedDropModes appropriately below. + // "CommonItemSetName": "common-table-custom1", + + // If this field is specified, it overrides the default rare item table for + // the duration of the quest. The rare item table name must begin with either + // "rare-table-" or "ItemRT-". As for common item sets, the rare table must + // be in system/item-tables/. If it's in JSON format, the table name must end + // with -v1, -v2, -v3, or -v4. If you use this, make sure to set + // AllowedDropModes appropriately below. + // "RareItemSetName": "rare-table-custom1", + + // If these fields are specified, they override the allowed drop modes and + // the default drop mode for the game when the quest is loaded. These + // function analogously to the drop mode fields in config.json; see the + // comments there for more information. If a custom common or rare table is + // also specified above, the client drop mode should be disallowed here (by + // clearing the 0x02 bit of AllowedDropModes). + // "AllowedDropModes": 0x1D, + // "DefaultDropMode": "SERVER_PRIVATE", }