From a0a7231d67976665f5b3233916fac36774ddd9fb Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 21 Dec 2025 21:06:29 -0800 Subject: [PATCH] reformat remaining files --- src/PSOEncryption.cc | 42 +- src/PSOEncryption.hh | 24 +- src/PSOProtocol.cc | 12 +- src/PSOProtocol.hh | 21 +- src/ProxyCommands.cc | 200 +- src/ProxySession.hh | 5 +- src/Quest.cc | 193 +- src/Quest.hh | 18 +- src/QuestMetadata.cc | 33 +- src/QuestMetadata.hh | 7 +- src/QuestScript.cc | 2280 +++++++++------------ src/QuestScript.hh | 14 +- src/RareItemSet.cc | 21 +- src/ReceiveCommands.cc | 494 ++--- src/ReceiveSubcommands.cc | 443 ++-- src/ReceiveSubcommands.hh | 5 +- src/ReplaySession.cc | 20 +- src/SaveFileFormats.cc | 118 +- src/SaveFileFormats.hh | 239 +-- src/SendCommands.cc | 284 +-- src/SendCommands.hh | 106 +- src/Server.hh | 20 +- src/ServerShell.cc | 9 +- src/ServerState.cc | 83 +- src/ServerState.hh | 16 +- src/ShellCommands.cc | 13 +- src/ShellCommands.hh | 3 +- src/StaticGameData.cc | 280 +-- src/TeamIndex.cc | 18 +- src/TeamIndex.hh | 6 +- src/Text.cc | 28 +- src/Text.hh | 15 +- src/TextIndex.cc | 17 +- src/Version.cc | 13 +- src/WordSelectTable.cc | 9 +- system/quests/hidden/q88530-gc-e.bin.txt | 16 +- system/quests/hidden/q88531-gc-e.bin.txt | 3 +- system/quests/hidden/q88532-gc3-e.bin.txt | 3 +- system/quests/hidden/q88533-gc3-e.bin.txt | 3 +- system/quests/retrieval/q058-gc-e.bin.txt | 173 +- 40 files changed, 2117 insertions(+), 3190 deletions(-) diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index a0ddc696..f2b71d3f 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -11,8 +11,6 @@ using namespace std; -// TODO: fix style in this file, especially in psobb functions - RandomGenerator::RandomGenerator(uint32_t seed) : initial_seed(seed) {} DisabledRandomGenerator::DisabledRandomGenerator() : RandomGenerator(0) {} @@ -77,8 +75,7 @@ void PSOLFGEncryption::encrypt_both_endian(void* le_vdata, void* be_vdata, size_ } } -PSOV2Encryption::PSOV2Encryption(uint32_t seed) - : PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) { +PSOV2Encryption::PSOV2Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH + 1, STREAM_LENGTH) { uint32_t a = 1, b = this->initial_seed; this->stream[0x37] = b; for (uint16_t virtual_index = 0x15; virtual_index <= 0x36 * 0x15; virtual_index += 0x15) { @@ -106,8 +103,7 @@ PSOEncryption::Type PSOV2Encryption::type() const { return Type::V2; } -PSOV3Encryption::PSOV3Encryption(uint32_t seed) - : PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) { +PSOV3Encryption::PSOV3Encryption(uint32_t seed) : PSOLFGEncryption(seed, STREAM_LENGTH, STREAM_LENGTH) { uint32_t x, y, basekey, source1, source2, source3; basekey = 0; @@ -154,9 +150,7 @@ PSOEncryption::Type PSOV3Encryption::type() const { return Type::V3; } -PSOBBEncryption::PSOBBEncryption( - const KeyFile& key, const void* original_seed, size_t seed_size) - : state(key) { +PSOBBEncryption::PSOBBEncryption(const KeyFile& key, const void* original_seed, size_t seed_size) : state(key) { this->apply_seed(original_seed, seed_size); } @@ -361,11 +355,10 @@ void PSOBBEncryption::tfs1_scramble(uint32_t* out1, uint32_t* out2) const { } void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) { - // Note: This part is done in the 03 command handler in the BB client, and - // isn't actually part of the encryption library. (Why did they do this?) + // Note: This part is done in the 03 command handler in the BB client, and isn't actually part of the encryption + // library. (Why did they do this?) string seed; - const uint8_t* original_seed_data = reinterpret_cast( - original_seed); + const uint8_t* original_seed_data = reinterpret_cast(original_seed); for (size_t x = 0; x < seed_size; x += 3) { seed.push_back(original_seed_data[x] ^ 0x19); seed.push_back(original_seed_data[x + 1] ^ 0x16); @@ -614,12 +607,8 @@ void PSOBBEncryption::apply_seed(const void* original_seed, size_t seed_size) { } PSOV2OrV3DetectorEncryption::PSOV2OrV3DetectorEncryption( - uint32_t key, - const std::unordered_set& v2_matches, - const std::unordered_set& v3_matches) - : key(key), - v2_matches(v2_matches), - v3_matches(v3_matches) {} + uint32_t key, const std::unordered_set& v2_matches, const std::unordered_set& v3_matches) + : key(key), v2_matches(v2_matches), v3_matches(v3_matches) {} void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size) { if (!this->active_crypt) { @@ -641,7 +630,8 @@ void PSOV2OrV3DetectorEncryption::encrypt(void* data, size_t size) { bool v3_match = this->v3_matches.count(decrypted_v3); if (!v2_match && !v3_match) { throw runtime_error(std::format( - "unable to determine crypt version (input={:08X}, v2={:08X}, v3={:08X})", encrypted, decrypted_v2, decrypted_v3)); + "unable to determine crypt version (input={:08X}, v2={:08X}, v3={:08X})", + encrypted, decrypted_v2, decrypted_v3)); } else if (v2_match && v3_match) { throw runtime_error(std::format("ambiguous crypt version (v2={:08X}, v3={:08X})", decrypted_v2, decrypted_v3)); } else if (v2_match) { @@ -665,8 +655,7 @@ PSOEncryption::Type PSOV2OrV3DetectorEncryption::type() const { PSOV2OrV3ImitatorEncryption::PSOV2OrV3ImitatorEncryption( uint32_t key, std::shared_ptr detector_crypt) - : key(key), - detector_crypt(detector_crypt) {} + : key(key), detector_crypt(detector_crypt) {} void PSOV2OrV3ImitatorEncryption::encrypt(void* data, size_t size) { if (!this->active_crypt) { @@ -761,9 +750,8 @@ shared_ptr PSOBBMultiKeyImitatorEncryption::ensure_crypt() { if (!key.get()) { throw logic_error("server crypt cannot be initialized because client crypt is not ready"); } - // Hack: JSD1 uses the client seed for both ends of the connection and - // ignores the server seed (though each end has its own state after that). - // To handle this, we use the other crypt's seed if the type is JSD1. + // Hack: JSD1 uses the client seed for both ends of the connection and ignores the server seed (though each end has + // its own state after that). To handle this, we use the other crypt's seed if the type is JSD1. if ((key->subtype == PSOBBEncryption::Subtype::JSD1) && this->jsd1_use_detector_seed) { const auto& detector_seed = this->detector_crypt->get_seed(); this->active_crypt = make_shared(*key, detector_seed.data(), detector_seed.size()); @@ -837,9 +825,7 @@ uint32_t encrypt_challenge_time(uint16_t value) { uint16_t decrypt_challenge_time(uint32_t value) { uint16_t mask = (value >> 0x10); uint8_t mask_one_bits = count_one_bits(mask); - return ((mask_one_bits < 4) || (mask_one_bits > 12)) - ? 0xFFFF - : ((mask ^ value) & 0xFFFF); + return ((mask_one_bits < 4) || (mask_one_bits > 12)) ? 0xFFFF : ((mask ^ value) & 0xFFFF); } string decrypt_v2_registry_value(const void* data, size_t size) { diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index d4ba34a6..ecbdf0db 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -159,8 +159,7 @@ public: }; struct KeyFile { - // initial_keys are actually a stream of uint32_ts, but we treat them as - // bytes for code simplicity + // initial_keys are actually a stream of uint32_ts, but we treat them as bytes for code simplicity union InitialKeys { uint8_t jsd1_stream_offset; parray as8; @@ -176,11 +175,9 @@ public: } __packed_ws__(PrivateKeys, 0x1000); InitialKeys initial_keys; PrivateKeys private_keys; - // This field only really needs to be one byte, but annoyingly, some - // compilers pad this structure to a longer alignment, presumably because - // the unions above contain structures with 32-bit alignment. To prevent - // this structure's size from not matching the .nsk files' sizes, we use - // an unnecessarily large size for this field. + // This field only really needs to be one byte, but annoyingly, some compilers pad this structure to a longer + // alignment, presumably because the unions above contain structures with 32-bit alignment. To prevent this + // structure's size from not matching the .nsk files' sizes, we use an unnecessarily large size for this field. le_uint64_t subtype; } __packed_ws__(KeyFile, 0x1050); @@ -198,8 +195,8 @@ protected: void apply_seed(const void* original_seed, size_t seed_size); }; -// The following classes provide support for automatically detecting which type -// of encryption a client is using based on their initial response to the server +// The following classes provide support for automatically detecting which type of encryption a client is using based +// on their initial response to the server class PSOV2OrV3DetectorEncryption : public PSOEncryption { public: @@ -234,9 +231,8 @@ protected: std::shared_ptr active_crypt; }; -// The following classes provide support for multiple PSOBB private keys, and -// the ability to automatically detect which key the client is using based on -// the first 8 bytes they send +// The following classes provide support for multiple PSOBB private keys, and the ability to automatically detect which +// key the client is using based on the first 8 bytes they send class PSOBBMultiKeyDetectorEncryption : public PSOEncryption { public: @@ -397,9 +393,7 @@ DecryptedPR2 decrypt_pr2_data(const std::string& data) { throw std::runtime_error("not enough data for PR2 header"); } phosg::StringReader r(data); - DecryptedPR2 ret = { - .compressed_data = data.substr(8), - .decompressed_size = r.get>()}; + DecryptedPR2 ret = {.compressed_data = data.substr(8), .decompressed_size = r.get>()}; PSOV2Encryption crypt(r.get>()); if (BE) { crypt.encrypt_big_endian(ret.compressed_data.data(), ret.compressed_data.size()); diff --git a/src/PSOProtocol.cc b/src/PSOProtocol.cc index 32ce49ad..b927b376 100644 --- a/src/PSOProtocol.cc +++ b/src/PSOProtocol.cc @@ -189,25 +189,19 @@ void PSOCommandHeader::set_flag(Version version, uint32_t flag) { void check_size_v(size_t size, size_t min_size, size_t max_size) { if (size < min_size) { throw std::runtime_error(std::format( - "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", - min_size, size)); + "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size)); } if (max_size < min_size) { max_size = min_size; } if (size > max_size) { throw std::runtime_error(std::format( - "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", - max_size, size)); + "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size)); } } std::string prepend_command_header( - Version version, - bool encryption_enabled, - uint16_t cmd, - uint32_t flag, - const std::string& data) { + Version version, bool encryption_enabled, uint16_t cmd, uint32_t flag, const std::string& data) { phosg::StringWriter ret; switch (version) { case Version::DC_NTE: diff --git a/src/PSOProtocol.hh b/src/PSOProtocol.hh index 7aba7440..45722ed3 100644 --- a/src/PSOProtocol.hh +++ b/src/PSOProtocol.hh @@ -46,23 +46,16 @@ union PSOCommandHeader { PSOCommandHeader(); } __packed_ws__(PSOCommandHeader, 8); -// This function is used in a lot of places to check received command sizes and -// cast them to the appropriate type +// This function is used in a lot of places to check received command sizes and cast them to the appropriate type template -RetT& check_size_generic( - PtrT data, - size_t size, - size_t min_size, - size_t max_size) { +RetT& check_size_generic(PtrT data, size_t size, size_t min_size, size_t max_size) { if (size < min_size) { throw std::runtime_error(std::format( - "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", - min_size, size)); + "command too small (expected at least 0x{:X} bytes, received 0x{:X} bytes)", min_size, size)); } if (size > max_size) { throw std::runtime_error(std::format( - "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", - max_size, size)); + "command too large (expected at most 0x{:X} bytes, received 0x{:X} bytes)", max_size, size)); } return *reinterpret_cast(data); } @@ -128,8 +121,4 @@ T* check_size_vec_t(std::string& data, size_t count, bool allow_extra = false) { void check_size_v(size_t size, size_t min_size, size_t max_size = 0); std::string prepend_command_header( - Version version, - bool encryption_enabled, - uint16_t cmd, - uint32_t flag, - const std::string& data); + Version version, bool encryption_enabled, uint16_t cmd, uint32_t flag, const std::string& data); diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 8aaa24d5..75ac1f6f 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -48,16 +48,12 @@ static void forward_command(shared_ptr c, bool to_server, const Channel: } } -// Command handlers. These are called to preprocess or react to specific -// commands in either direction. The functions have abbreviated names in order -// to make the massive table more readable. The functions' names are, in -// general, _[VERSIONS]_, where denotes who sent the -// command, VERSIONS denotes which versions this handler is for (with shortcuts -// - so v123 refers to all non-BB versions, for example, and DGX refers to all -// console versions), and COMMAND-NUMBERS are the hexadecimal value in the -// command header field that this handler is called for. If VERSIONS is omitted, -// the command handler is for all versions (for example, the 97 handler is like -// this). +// Command handlers. These are called to preprocess or react to specific commands in either direction. The functions +// have abbreviated names in order to make the massive table more readable. The functions' names are, in general, +// _[VERSIONS]_, where denotes who sent the command, VERSIONS denotes which versions this +// handler is for (with shortcuts - so v123 refers to all non-BB versions, for example, and DGX refers to all console +// versions), and COMMAND-NUMBERS are the hexadecimal value in the command header field that this handler is called +// for. If VERSIONS is omitted, the command handler is for all versions (for example, the 97 handler is like this). static asio::awaitable default_handler(shared_ptr, Channel::Message&) { co_return HandlerResult::FORWARD; @@ -88,8 +84,8 @@ static asio::awaitable S_1D(shared_ptr c, Channel::Messag } static asio::awaitable S_97(shared_ptr c, Channel::Message&) { - // We always assume a 97 has already been received by the client - we should - // have sent 97 01 before sending the client to the proxy server. + // We always assume a 97 has already been received by the client - we should have sent 97 01 before sending the + // client to the proxy server. c->proxy_session->server_channel->send(0xB1, 0x00); co_return HandlerResult::SUPPRESS; } @@ -208,9 +204,8 @@ static void send_9E_XB_to_server(std::shared_ptr c) { } static asio::awaitable S_G_9A(shared_ptr c, Channel::Message&) { - // TODO: Either delete this handler or finish implementing it (flag=00/02 - // should do the below, 01 should send 9C, anything else should end the - // session) + // TODO: Either delete this handler or finish implementing it (flag=00/02 should do the below, 01 should send 9C, + // anything else should end the session) C_LoginExtended_GC_9E cmd; if (c->proxy_session->remote_guild_card_number < 0) { cmd.player_tag = 0xFFFF0000; @@ -232,8 +227,7 @@ static asio::awaitable S_G_9A(shared_ptr c, Channel::Mess cmd.login_character_name.encode(c->login_character_name, c->language()); cmd.client_config = c->proxy_session->remote_client_config_data; - // If there's a guild card number, a shorter 9E is sent that ends - // right after the client config data + // If there's a guild card number, a shorter 9E is sent that ends right after the client config data c->proxy_session->server_channel->send( 0x9E, 0x01, &cmd, cmd.is_extended ? sizeof(C_LoginExtended_GC_9E) : sizeof(C_Login_PC_GC_9E)); @@ -246,8 +240,7 @@ static asio::awaitable S_V123U_02_17(shared_ptr c, Channe throw invalid_argument("patch server sent 17 server init"); } - // Most servers don't include after_message or have a shorter - // after_message than newserv does, so don't require it + // Most servers don't include after_message or have a shorter after_message than newserv does, so don't require it const auto& cmd = msg.check_size_t(0xFFFF); // This isn't forwarded to the client, so don't recreate the client's crypts @@ -259,9 +252,8 @@ static asio::awaitable S_V123U_02_17(shared_ptr c, Channe c->proxy_session->server_channel->crypt_out = make_shared(cmd.client_key); } - // Respond with an appropriate login command. We don't let the client do this - // because it believes it already did (when it was in an unlinked session, or - // in the patch server case, during the current session due to a hidden + // Respond with an appropriate login command. We don't let the client do this because it believes it already did + // (when it was in an unlinked session, or in the patch server case, during the current session due to a hidden // redirect). switch (c->version()) { case Version::PC_PATCH: @@ -332,15 +324,12 @@ static asio::awaitable S_U_04(shared_ptr c, Channel::Mess } static asio::awaitable S_B_03(shared_ptr c, Channel::Message& msg) { - // Most servers don't include after_message or have a shorter after_message - // than newserv does, so don't require it + // Most servers don't include after_message or have a shorter after_message than newserv does, so don't require it const auto& cmd = msg.check_size_t(0xFFFF); - // This isn't forwarded to the client, so only recreate the server's crypts. - // Use the same crypt type as the client... the server has the luxury of - // being able to try all the crypts it knows to detect what type the client - // uses, but the client can't do this since it sends the first encrypted - // data on the connection. + // This isn't forwarded to the client, so only recreate the server's crypts. Use the same crypt type as the client... + // the server has the luxury of being able to try all the crypts it knows to detect what type the client uses, but + // the client can't do this since it sends the first encrypted data on the connection. if (!c->bb_detector_crypt) { throw logic_error("Client proxy session started with missing detector crypt"); } @@ -394,44 +383,38 @@ static asio::awaitable S_V123_04(shared_ptr c, Channel::M co_return HandlerResult::SUPPRESS; } - // Some servers send a short 04 command if they don't use all of the 0x20 - // bytes available. We should be prepared to handle that. + // Some servers send a short 04 command if they don't use all of the 0x20 bytes available. We should be prepared to + // handle that. auto& cmd = msg.check_size_t( - offsetof(S_UpdateClientConfig_V3_04, client_config), - sizeof(S_UpdateClientConfig_V3_04)); + offsetof(S_UpdateClientConfig_V3_04, client_config), sizeof(S_UpdateClientConfig_V3_04)); - // If this is a logged-in session, hide the guild card number assigned by the - // remote server so the client doesn't see it change. If this is a logged-out - // session, then the client never received a guild card number from newserv + // If this is a logged-in session, hide the guild card number assigned by the remote server so the client doesn't see + // it change. If this is a logged-out session, then the client never received a guild card number from newserv // anyway, so we can let the client see the number from the remote server. bool had_guild_card_number = (c->proxy_session->remote_guild_card_number >= 0); if (c->proxy_session->remote_guild_card_number != cmd.guild_card_number) { c->proxy_session->remote_guild_card_number = cmd.guild_card_number; c->log.info_f("Remote guild card number set to {}", c->proxy_session->remote_guild_card_number); string message = std::format( - "The remote server\nhas assigned your\nGuild Card number:\n$C6{}", - c->proxy_session->remote_guild_card_number); + "The remote server\nhas assigned your\nGuild Card number:\n$C6{}", c->proxy_session->remote_guild_card_number); send_ship_info(c->channel, message); } if (c->login) { cmd.guild_card_number = c->login->account->account_id; } - // It seems the client ignores the length of the 04 command, and always copies - // 0x20 bytes to its config data. So if the server sends a short 04 command, - // part of the previous command ends up in the security data (usually part of - // the copyright string from the server init command). We simulate that here. - // If there was previously a guild card number, assume we got the lobby server - // init text instead of the port map init text. + // It seems the client ignores the length of the 04 command, and always copies 0x20 bytes to its config data. So if + // the server sends a short 04 command, part of the previous command ends up in the security data (usually part of + // the copyright string from the server init command), which we simulate here. If there was previously a guild card + // number, assume we got the lobby server init text instead of the port map init text. memcpy(c->proxy_session->remote_client_config_data.data(), had_guild_card_number ? "t Lobby Server. Copyright SEGA E" : "t Port Map. Copyright SEGA Enter", 0x20); memcpy(c->proxy_session->remote_client_config_data.data(), &cmd.client_config, min(msg.data.size() - offsetof(S_UpdateClientConfig_V3_04, client_config), c->proxy_session->remote_client_config_data.bytes())); - // If the guild card number was not set, pretend (to the server) that this is - // the first 04 command the client has received. The client responds with a 96 - // (checksum) in that case. + // If the guild card number was not set, pretend (to the server) that this is the first 04 command the client has + // received. The client responds with a 96 (checksum) in that case. if (!had_guild_card_number) { le_uint64_t checksum = phosg::random_object() & 0x0000FFFFFFFFFFFF; c->proxy_session->server_channel->send(0x96, 0x00, &checksum, sizeof(checksum)); @@ -450,9 +433,8 @@ static asio::awaitable S_V123_06(shared_ptr c, Channel::M } } - // If the session is Ep3, and Unmask Whispers is on, and there's enough data, - // and the message has private_flags, and the private_flags say that you - // shouldn't see the message, then change the private_flags + // If the session is Ep3, and Unmask Whispers is on, and there's enough data, and the message has private_flags, and + // the private_flags say that you shouldn't see the message, then change the private_flags if (is_ep3(c->version()) && c->check_flag(Client::Flag::PROXY_EP3_UNMASK_WHISPERS) && (msg.data.size() >= 12) && @@ -523,8 +505,8 @@ constexpr on_message_t S_P_81 = &S_81; constexpr on_message_t S_B_81 = &S_81; static asio::awaitable S_88(shared_ptr c, Channel::Message& msg) { - // If the client isn't in the lobby, suppress the command (Ep3 can crash if - // it receives this while loading; other versions probably also will crash) + // If the client isn't in the lobby, suppress the command (Ep3 can crash if it receives this while loading; other + // versions probably also will crash) if (!c->proxy_session->is_in_lobby) { co_return HandlerResult::SUPPRESS; } @@ -544,8 +526,7 @@ static asio::awaitable S_88(shared_ptr c, Channel::Messag } static asio::awaitable S_B1(shared_ptr c, Channel::Message&) { - // Block all time updates from the remote server, so client's time remains - // consistent + // Block all time updates from the remote server, so client's time remains consistent c->proxy_session->server_channel->send(0x99, 0x00); co_return HandlerResult::SUPPRESS; } @@ -597,7 +578,6 @@ static asio::awaitable S_B2(shared_ptr c, Channel::Messag using FooterT = RELFileFooterT; - // TODO: Support SH-4 disassembly too bool is_ppc = ::is_ppc(c->version()); bool is_x86 = ::is_x86(c->version()); bool is_sh4 = ::is_sh4(c->version()); @@ -713,8 +693,7 @@ static asio::awaitable S_C4(shared_ptr c, Channel::Messag bool modified = false; if (c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) { size_t expected_size = sizeof(CmdT) * msg.flag; - // Some servers (e.g. Schtserv) send extra data on the end of this command; - // the client ignores it so we can ignore it too + // Schtserv sends extra data on the end of this command; the client ignores it so we can ignore it too auto* entries = &msg.check_size_t(expected_size, 0xFFFF); for (size_t x = 0; x < msg.flag; x++) { if (entries[x].guild_card_number == c->proxy_session->remote_guild_card_number) { @@ -745,15 +724,12 @@ static asio::awaitable S_G_E4(shared_ptr c, Channel::Mess } static asio::awaitable S_B_22(shared_ptr c, Channel::Message& msg) { - // We use this command (which is sent before the init encryption command) to - // detect a particular server behavior that we'll have to work around later. - // It looks like this command's existence is an anti-proxy measure, since - // this command is 0x34 bytes in total, and the logic that adds padding bytes - // when the command size isn't a multiple of 8 is only active when encryption - // is enabled. Presumably some simpler proxies would get this wrong. - // Editor's note: There's an unsavory message in this command's data field, - // hence the hash here instead of a direct string comparison. I'd love to - // hear the story behind why they put that string there. + // We use this command (which is sent before the init encryption command) to detect a particular server behavior that + // we'll have to work around later. It looks like this command's existence is an anti-proxy measure, since this + // command is 0x34 bytes in total, and the logic that adds padding bytes when the command size isn't a multiple of 8 + // is only active when encryption is enabled. Presumably some simpler proxies would get this wrong. + // Editor's note: There's an unsavory message in this command's data field, hence the hash here instead of a direct + // string comparison. I'd love to hear the story behind why they put that string there. if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) { c->log.info_f("Enabling remote IP CRC patch"); c->proxy_session->enable_remote_ip_crc_patch = true; @@ -762,12 +738,10 @@ static asio::awaitable S_B_22(shared_ptr c, Channel::Mess } static asio::awaitable S_19_U_14(shared_ptr c, Channel::Message& msg) { - // If the command is shorter than 6 bytes, use the previous server command to - // fill it in. This simulates a behavior used by some private servers where a - // longer previous command is used to fill part of the client's receive buffer - // with meaningful data, then an intentionally undersize 19 command is sent - // which results in the client using the previous command's data as part of - // the 19 command's contents. They presumably do this in an attempt to prevent + // If the command is shorter than 6 bytes, use the previous server command to fill it in. This simulates a behavior + // used by some private servers where a longer previous command is used to fill part of the client's receive buffer + // with meaningful data, then an intentionally undersize 19 command is sent which results in the client using the + // previous command's data as part of the 19 command's contents. They presumably do this in an attempt to prevent // people from using proxies. if (msg.data.size() < sizeof(c->proxy_session->prev_server_command_bytes)) { msg.data.append( @@ -792,8 +766,8 @@ static asio::awaitable S_19_U_14(shared_ptr c, Channel::M auto& cmd = msg.check_size_t(0xFFFF); new_ep = make_endpoint_ipv6(cmd.address.data(), cmd.port); } else { - // This weird maximum size is here to properly handle the version-split - // command that some servers (including newserv) use on port 9100 + // This weird maximum size is here to properly handle the version-split command that some servers (including + // newserv) use on port 9100 auto& cmd = msg.check_size_t(0xFFFF); new_ep = make_endpoint_ipv4(cmd.address, cmd.port); } @@ -824,9 +798,8 @@ static asio::awaitable S_19_U_14(shared_ptr c, Channel::M } static asio::awaitable S_V3_1A_D5(shared_ptr c, Channel::Message&) { - // If the client is a version that sends close confirmations and the client - // has the no-close-confirmation flag set in its newserv client config, send a - // fake confirmation to the remote server immediately. + // If the client is a version that sends close confirmations and the client has the no-close-confirmation flag set in + // its newserv client config, send a fake confirmation to the remote server immediately. if (is_v3(c->version()) && c->check_flag(Client::Flag::NO_D6)) { c->proxy_session->server_channel->send(0xD6); } @@ -1160,8 +1133,7 @@ static asio::awaitable S_6x(shared_ptr c, Channel::Messag static asio::awaitable C_GXB_61(shared_ptr c, Channel::Message& msg) { bool modified = false; - // TODO: We should check if the info board text was actually modified and - // return MODIFIED if so. + // TODO: We should check if the info board text was actually modified and return MODIFIED if so. if (is_v4(c->version())) { auto& pd = msg.check_size_t(0xFFFF); @@ -1171,9 +1143,8 @@ static asio::awaitable C_GXB_61(shared_ptr c, Channel::Me C_CharacterData_V3_61_98* pd; if (msg.flag == 4) { // Episode 3 auto& ep3_pd = msg.check_size_t(); - // Technically we could decrypt the Ep3 config struct within the player - // data, but this may confuse some non-newserv upstream servers if they - // implement this structure incorrectly. The decryption would go like: + // Technically we could decrypt the Ep3 config struct within the player data, but this may confuse the upstream + // server if it implements this structure incorrectly. The decryption would go like: // if (ep3_pd.ep3_config.is_encrypted) { // decrypt_trivial_gci_data( // &ep3_pd.ep3_config.card_counts, @@ -1206,8 +1177,7 @@ static asio::awaitable C_GX_D9(shared_ptr, Channel::Messa while (msg.data.size() & 3) { msg.data.push_back(0); } - // TODO: We should check if the info board text was actually modified and - // return FORWARD if not. + // TODO: We should check if the info board text was actually modified and return FORWARD if not. co_return HandlerResult::MODIFIED; } @@ -1226,8 +1196,7 @@ static asio::awaitable C_B_D9(shared_ptr c, Channel::Mess } catch (const runtime_error& e) { c->log.warning_f("Failed to decode and unescape D9 command: {}", e.what()); } - // TODO: We should check if the info board text was actually modified and - // return HandlerResult::FORWARD if not. + // TODO: We should check if the info board text was actually modified and return HandlerResult::FORWARD if not. co_return HandlerResult::MODIFIED; } @@ -1250,8 +1219,8 @@ static asio::awaitable S_44_A6(shared_ptr c, Channel::Mes } else { basename = filename; } - output_filename = std::format("{}.{}.{}{}", - basename, is_download ? "download" : "online", phosg::now(), extension); + output_filename = std::format( + "{}.{}.{}{}", basename, is_download ? "download" : "online", phosg::now(), extension); for (size_t x = 0; x < output_filename.size(); x++) { if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') { @@ -1420,8 +1389,8 @@ static asio::awaitable S_G_B8(shared_ptr c, Channel::Mess c->log.info_f("Wrote {} bytes to {}", size, output_filename); } - // Unset the flag specifying that the client has newserv's card definitions, - // so the file sill be sent again if the client returns to newserv. + // Unset the flag specifying that the client has newserv's card definitions, so the file sill be sent again if the + // client returns to newserv. c->clear_flag(Client::Flag::HAS_EP3_CARD_DEFS); co_return is_ep3(c->version()) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS; @@ -1454,8 +1423,7 @@ static asio::awaitable S_G_B9(shared_ptr c, Channel::Mess } } - // This command exists only in final Episode 3 and not in Trial Edition - // (hence not using is_ep3() here) + // This command exists only in final Episode 3 and not in Trial Edition (hence not using is_ep3() here) co_return (c->version() == Version::GC_EP3) ? HandlerResult::FORWARD : HandlerResult::SUPPRESS; } @@ -1483,8 +1451,7 @@ static asio::awaitable S_G_EF(shared_ptr c, Channel::Mess } static asio::awaitable S_B_EF(shared_ptr, Channel::Message&) { - // See the comments on EF in CommandFormats.hh for why we unconditionally - // suppress these. + // See the comments on EF in CommandFormats.hh for why we unconditionally suppress these. co_return HandlerResult::SUPPRESS; } @@ -1526,11 +1493,6 @@ static asio::awaitable S_65_67_68_EB(shared_ptr c, Channe c->proxy_session->item_creator.reset(); c->proxy_session->map_state.reset(); - // This command can cause the client to no longer send D6 responses when - // 1A/D5 large message boxes are closed. newserv keeps track of this - // behavior in the client config, so if it happens during a proxy session, - // update the client config that we'll restore if the client uses the change - // ship or change block command. if (c->check_flag(Client::Flag::NO_D6_AFTER_LOBBY)) { c->set_flag(Client::Flag::NO_D6); } @@ -1557,8 +1519,7 @@ static asio::awaitable S_65_67_68_EB(shared_ptr c, Channe modified = true; } } else if (c->check_flag(Client::Flag::PROXY_PLAYER_NOTIFICATIONS_ENABLED) && (msg.command != 0x67)) { - send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}", - index, entry.lobby_data.guild_card_number, name); + send_text_message_fmt(c->channel, "$C6Join: {}/{}\n{}", index, entry.lobby_data.guild_card_number, name); } auto& p = c->proxy_session->lobby_players[index]; p.guild_card_number = entry.lobby_data.guild_card_number; @@ -1566,8 +1527,7 @@ static asio::awaitable S_65_67_68_EB(shared_ptr c, Channe p.language = entry.inventory.language; p.section_id = entry.disp.visual.section_id; p.char_class = entry.disp.visual.char_class; - c->log.info_f("Added lobby player: ({}) {} {}", - index, p.guild_card_number, p.name); + c->log.info_f("Added lobby player: ({}) {} {}", index, p.guild_card_number, p.name); } } if (num_replacements > 1) { @@ -1647,8 +1607,7 @@ static asio::awaitable S_64(shared_ptr c, Channel::Messag cmd = &msg.check_size_t(sizeof(S_JoinGame_Ep3_64)); cmd_ep3 = &msg.check_size_t(); } else if (c->version() == Version::XB_V3) { - // Schtserv doesn't send the unknown_a1 field in this command, and we don't - // use it here, so we allow it to be omitted. + // Schtserv doesn't send the unknown_a1 field here, and we don't use it, so we allow it to be omitted. cmd = &msg.check_size_t(sizeof(CmdT) - 0x18, sizeof(CmdT)); } else { cmd = &msg.check_size_t(0xFFFF); @@ -1665,8 +1624,8 @@ static asio::awaitable S_64(shared_ptr c, Channel::Messag c->proxy_session->lobby_event = cmd->event; c->proxy_session->lobby_difficulty = cmd->difficulty; c->proxy_session->lobby_section_id = cmd->section_id; - // We only need the game mode for overriding drops, and SOLO behaves the same - // as NORMAL in that regard, so we can conveniently ignore SOLO here + // We only need the game mode for overriding drops, and SOLO behaves the same as NORMAL in that regard, so we can + // conveniently ignore SOLO here if (cmd->battle_mode) { c->proxy_session->lobby_mode = GameMode::BATTLE; } else if (cmd->challenge_mode) { @@ -1703,8 +1662,7 @@ static asio::awaitable S_64(shared_ptr c, Channel::Messag } if (c->version() == Version::GC_NTE) { - // GC NTE ignores the variations field entirely, so clear the array to - // ensure we'll load the correct maps + // GC NTE ignores the variations field entirely, so clear the array to ensure we'll load the correct maps cmd->variations = Variations(); } @@ -1713,7 +1671,10 @@ static asio::awaitable S_64(shared_ptr c, Channel::Messag c->proxy_session->set_drop_mode(s, c->version(), c->override_random_seed, c->proxy_session->drop_mode); if (!is_ep3(c->version()) && (c->proxy_session->lobby_mode != GameMode::CHALLENGE)) { auto supermaps = s->supermaps_for_variations( - c->proxy_session->lobby_episode, c->proxy_session->lobby_mode, c->proxy_session->lobby_difficulty, cmd->variations); + c->proxy_session->lobby_episode, + c->proxy_session->lobby_mode, + c->proxy_session->lobby_difficulty, + cmd->variations); c->proxy_session->map_state = make_shared( c->id, c->proxy_session->lobby_difficulty, @@ -1831,8 +1792,8 @@ static asio::awaitable S_AC(shared_ptr c, Channel::Messag } static asio::awaitable S_66_69_E9(shared_ptr c, Channel::Message& msg) { - // Schtserv sends a large command here for unknown reasons. The client ignores - // the extra data, so we allow the large command here. + // Schtserv sends a large command here for unknown reasons. The client ignores the extra data, so we allow the large + // command here. const auto& cmd = msg.check_size_t(0xFFFF); size_t index = cmd.client_id; if (index >= c->proxy_session->lobby_players.size()) { @@ -1996,9 +1957,8 @@ asio::awaitable C_6x(shared_ptr c, Channel::Message& msg) break; case 0x06: - // On BB, the 6x06 command is blank - the server generates the actual - // Guild Card contents and sends it to the target client, so we only - // expect data here if the client isn't BB. + // On BB, the 6x06 command is blank - the server generates the actual Guild Card contents and sends it to the + // target client, so we only expect data here if the client isn't BB. if (!is_v4(c->version()) && c->login && c->login->account->account_id != c->proxy_session->remote_guild_card_number) { @@ -2134,8 +2094,8 @@ constexpr on_message_t C_X_6x = &C_6x; constexpr on_message_t C_B_6x = &C_6x; static asio::awaitable C_V123_A0_A1(shared_ptr c, Channel::Message&) { - // We override Change Ship and Change Block to send the player back to the - // original server (ending the proxy session), except on BB. + // We override Change Ship and Change Block to send the player back to the original server (ending the proxy + // session), except on BB. c->proxy_session->server_channel->disconnect(); co_return HandlerResult::SUPPRESS; } @@ -2454,8 +2414,7 @@ asio::awaitable on_proxy_command(shared_ptr c, bool from_server, u asio::awaitable handle_proxy_server_commands( shared_ptr c, shared_ptr ses, shared_ptr channel) { std::string error_str; - // server_channel can be changed by receiving a 19 command, hence the - // exception handler is inside the loop here + // server_channel can be changed by receiving a 19 command, hence the exception handler is inside the loop here while ((c->proxy_session == ses) && (ses->server_channel == channel) && channel->connected()) { unique_ptr msg; try { @@ -2472,8 +2431,7 @@ asio::awaitable handle_proxy_server_commands( if (ec == asio::error::eof || ec == asio::error::connection_reset) { error_str = "Server channel\ndisconnected"; } else if (ec == asio::error::operation_aborted) { - // This happens when the player chooses Change Ship/Change Block, so we - // don't show an error message + // This happens when the player chooses Change Ship/Change Block, so we don't show an error message } else { error_str = e.what(); } diff --git a/src/ProxySession.hh b/src/ProxySession.hh index 854940cb..c88fe550 100644 --- a/src/ProxySession.hh +++ b/src/ProxySession.hh @@ -55,9 +55,8 @@ struct ProxySession { std::shared_ptr map_state; std::shared_ptr last_bin_contents; std::shared_ptr last_dat_contents; - // Note: We intentionally don't use the client's item ID space because the - // client may create items at the same time as the proxy, so server/client - // state could go out of sync + // Note: We intentionally don't use the client's item ID space because the client may create items at the same time + // as the proxy, so server/client state could go out of sync uint32_t next_item_id = 0x44000000; struct PersistentConfig { diff --git a/src/Quest.cc b/src/Quest.cc index 655c7b48..8297e900 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -22,8 +22,7 @@ using namespace std; -QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json) - : category_id(category_id) { +QuestCategoryIndex::Category::Category(uint32_t category_id, const phosg::JSON& json) : category_id(category_id) { this->enabled_flags = json.get_int(0); this->directory_name = json.get_string(1); this->name = json.get_string(2); @@ -46,9 +45,8 @@ shared_ptr QuestCategoryIndex::at(uint32_t c template struct PSOMemCardDLQFileEncryptedHeaderT { U32T round2_seed; - // To compute checksum, set checksum to zero, then compute the CRC32 of the - // entire data section, including this header struct (but not the unencrypted - // header struct). + // To compute checksum, set checksum to zero, then compute the CRC32 of the entire data section, including this + // header struct (but not the unencrypted header struct). U32T checksum; le_uint32_t decompressed_size; le_uint32_t round3_seed; @@ -67,15 +65,12 @@ string decrypt_download_quest_data_section( size_t orig_size = decrypted.size(); decrypted.resize((decrypted.size() + 3) & (~3)); - // Note: Other PSO save files have the round 2 seed at the end of the data, - // not at the beginning. Presumably they did this because the system, - // character, and Guild Card files are a constant size, but download quest - // files can vary in size. + // Other PSO save files have the round 2 seed at the end, not at the beginning. Presumably this is because the + // system, character, and Guild Card files are a constant size, but download quest files can vary in size. using HeaderT = PSOMemCardDLQFileEncryptedHeaderT; auto* header = reinterpret_cast(decrypted.data()); PSOV2Encryption round2_crypt(header->round2_seed); - round2_crypt.encrypt_t( - decrypted.data() + 4, (decrypted.size() - 4)); + round2_crypt.encrypt_t(decrypted.data() + 4, (decrypted.size() - 4)); if (is_ep3_trial) { phosg::StringReader r(decrypted); @@ -85,9 +80,8 @@ string decrypt_download_quest_data_section( } r.skip(9); - // Some Ep3 trial download quests don't have a stop opcode in the PRS - // stream; it seems the client just automatically stops when the correct - // amount of data has been produced. To handle this, we allow the PRS stream + // Some Ep3 trial download quests don't have a stop opcode in the PRS stream; it seems the client just + // automatically stops when the correct amount of data has been produced. To handle this, we allow the PRS stream // to be unterminated here. size_t decompressed_size = prs_decompress_size( r.getv(r.remaining(), false), r.remaining(), sizeof(Episode3::MapDefinitionTrial), true); @@ -100,8 +94,7 @@ string decrypt_download_quest_data_section( } else { if (header->decompressed_size & 0xFFF00000) { - throw runtime_error(std::format( - "decompressed_size too large ({:08X})", header->decompressed_size)); + throw runtime_error(std::format("decompressed_size too large ({:08X})", header->decompressed_size)); } if (!skip_checksum) { @@ -111,29 +104,23 @@ string decrypt_download_quest_data_section( header->checksum = expected_crc; if (expected_crc != actual_crc && expected_crc != phosg::bswap32(actual_crc)) { throw runtime_error(std::format( - "incorrect decrypted data section checksum: expected {:08X}; received {:08X}", - expected_crc, actual_crc)); + "incorrect decrypted data section checksum: expected {:08X}; received {:08X}", expected_crc, actual_crc)); } } - // Unlike the above rounds, round 3 is always little-endian (it corresponds to - // the round of encryption done on the server before sending the file to the - // client in the first place) + // Unlike the above rounds, round 3 is always little-endian (it corresponds to the round of encryption done on the + // server before sending the file to the client in the first place) PSOV2Encryption(header->round3_seed).decrypt(decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT)); decrypted.resize(orig_size); - // Some download quest GCI files have decompressed_size fields that are 8 - // bytes smaller than the actual decompressed size of the data. They seem to - // work fine, so we accept both cases as correct. + // Some download quest GCI files have decompressed_size fields that are 8 bytes smaller than the actual + // decompressed size of the data. They seem to work fine, so we accept both cases as correct. size_t decompressed_size = prs_decompress_size( - decrypted.data() + sizeof(HeaderT), - decrypted.size() - sizeof(HeaderT)); + decrypted.data() + sizeof(HeaderT), decrypted.size() - sizeof(HeaderT)); size_t expected_decompressed_size = header->decompressed_size; - if ((decompressed_size != expected_decompressed_size) && - (decompressed_size != expected_decompressed_size - 8)) { + if ((decompressed_size != expected_decompressed_size) && (decompressed_size != expected_decompressed_size - 8)) { throw runtime_error(std::format( - "decompressed size ({}) does not match expected size ({})", - decompressed_size, expected_decompressed_size)); + "decompressed size ({}) does not match expected size ({})", decompressed_size, expected_decompressed_size)); } return decrypted.substr(sizeof(HeaderT)); @@ -169,8 +156,7 @@ string find_seed_and_decrypt_download_quest_data_section( string result; uint64_t result_seed = phosg::parallel_range_blocks([&](uint64_t seed, size_t) { try { - string ret = decrypt_download_quest_data_section( - data_section, size, seed, skip_checksum, is_ep3_trial); + string ret = decrypt_download_quest_data_section(data_section, size, seed, skip_checksum, is_ep3_trial); lock_guard g(result_lock); result = std::move(ret); return true; @@ -300,8 +286,7 @@ string VersionedQuest::encode_qst() const { return encode_qst_file(files, this->meta.name, this->meta.quest_number, xb_filename, this->meta.version, this->is_dlq_encoded); } -Quest::Quest(shared_ptr initial_version) - : meta(initial_version->meta), supermap(nullptr) { +Quest::Quest(shared_ptr initial_version) : meta(initial_version->meta), supermap(nullptr) { this->add_version(initial_version); } @@ -320,10 +305,7 @@ phosg::JSON Quest::json() const { })); } - return phosg::JSON::dict({ - {"Metadata", this->meta.json()}, - {"Versions", std::move(versions_json)}, - }); + return phosg::JSON::dict({{"Metadata", this->meta.json()}, {"Versions", std::move(versions_json)}}); } uint32_t Quest::versions_key(Version v, Language language) { @@ -430,9 +412,9 @@ shared_ptr Quest::version(Version v, Language language) co return it->second; } -QuestIndex::QuestIndex(const string& directory, shared_ptr category_index, bool raise_on_any_failure) - : directory(directory), - category_index(category_index) { +QuestIndex::QuestIndex( + const string& directory, shared_ptr category_index, bool raise_on_any_failure) + : directory(directory), category_index(category_index) { struct FileData { string filename; @@ -462,9 +444,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptrsize() & 0x3FF)) { data_ptr->push_back(0x00); } @@ -587,9 +568,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr> parsed_json_files; for (auto& [basename, entry] : bin_files) { try { @@ -601,8 +581,7 @@ QuestIndex::QuestIndex(const string& directory, shared_ptr filename_tokens = phosg::split(basename, '-'); @@ -653,8 +632,8 @@ QuestIndex::QuestIndex(const string& directory, shared_ptrmeta, bin_decompressed.data(), bin_decompressed.size(), vq->meta.version, vq->meta.language); - // Find the corresponding dat and pvr files with the same basename as the - // bin file; if not found, look for them without the language suffix + // Find the corresponding dat and pvr files with the same basename as the bin file; if not found, look for them + // without the language suffix const DATFileData* dat_filedata = nullptr; const FileData* pvr_filedata = nullptr; try { @@ -672,8 +651,7 @@ QuestIndex::QuestIndex(const string& directory, shared_ptrbin_contents = entry.data; @@ -798,10 +776,7 @@ shared_ptr QuestIndex::get(const std::string& name) const { } vector> QuestIndex::categories( - QuestMenuType menu_type, - Episode episode, - uint16_t version_flags, - IncludeCondition include_condition) const { + QuestMenuType menu_type, Episode episode, uint16_t version_flags, IncludeCondition include_condition) const { vector> ret; for (const auto& cat : this->category_index->categories) { if (cat->check_flag(menu_type) && !this->filter(episode, version_flags, cat->category_id, include_condition, 1).empty()) { @@ -852,9 +827,8 @@ vector>> QuestIndex::filt } string encode_download_quest_data(const string& compressed_data, size_t decompressed_size, uint32_t encryption_seed) { - // Download quest files are like normal (PRS-compressed) quest files, but they - // are encrypted with PSO V2 encryption (even on V3 / PSO GC), and a small - // header (PSODownloadQuestHeader) is prepended to the encrypted data. + // Download quest files are like normal (PRS-compressed) quest files, but they are encrypted with PSO V2 encryption + // (even on V3 / PSO GC), and a small header (PSODownloadQuestHeader) is prepended to the encrypted data. if (encryption_seed == 0) { encryption_seed = phosg::random_object(); @@ -869,8 +843,7 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre header->encryption_seed = encryption_seed; data += compressed_data; - // Add temporary extra bytes if necessary so encryption won't fail - the data - // size must be a multiple of 4 for PSO V2 encryption. + // Add extra bytes if necessary so encryption won't fail; the data size must be a multiple of 4 for PSO V2 encryption size_t original_size = data.size(); data.resize((data.size() + 3) & (~3)); @@ -882,9 +855,8 @@ string encode_download_quest_data(const string& compressed_data, size_t decompre } shared_ptr VersionedQuest::create_download_quest(Language override_language) const { - // The download flag needs to be set in the bin header, or else the client - // will ignore it when scanning for download quests in an offline game. To set - // this flag, we need to decompress the quest's .bin file, set the flag, then + // The download flag needs to be set in the bin header, or else the client will ignore it when scanning for download + // quests in an offline game. To set this flag, we need to decompress the quest's .bin file, set the flag, then // recompress it again. string decompressed_bin = prs_decompress(*this->bin_contents); @@ -934,8 +906,7 @@ shared_ptr VersionedQuest::create_download_quest(Language overri string compressed_bin = prs_compress(decompressed_bin); - // Return a new VersionedQuest object with appropriately-processed .bin and - // .dat file contents + // Return a new VersionedQuest object with appropriately-processed .bin and .dat file contents auto dlq = make_shared(*this); dlq->bin_contents = make_shared(encode_download_quest_data(compressed_bin, decompressed_bin.size())); dlq->dat_contents = make_shared(encode_download_quest_data(*this->dat_contents)); @@ -944,20 +915,15 @@ shared_ptr VersionedQuest::create_download_quest(Language overri return dlq; } -string decode_gci_data( - const string& data, - ssize_t find_seed_num_threads, - int64_t known_seed, - bool skip_checksum) { +string decode_gci_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) { phosg::StringReader r(data); const auto& header = r.get(); header.check(); if (header.is_ep12()) { const auto& dlq_header = r.get(false); - // Unencrypted GCI files appear to always have zeroes in these fields. - // Encrypted GCI files are highly unlikely to have zeroes in ALL of these - // fields, so assume it's encrypted if any of them are nonzero. + // Unencrypted GCI files appear to always have zeroes in these fields. Encrypted GCI files are highly unlikely to + // have zeroes in ALL of these fields, so assume it's encrypted if any of them are nonzero. if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) { if (known_seed >= 0) { return decrypt_download_quest_data_section( @@ -1010,14 +976,13 @@ string decode_gci_data( } } else { - // The first 0x10 bytes in the data segment appear to be unused. In most - // files I've seen, the last half of it (8 bytes) are duplicates of the - // first 8 bytes of the unscrambled, compressed data, though this is the - // result of an uninitialized memory bug when the client encodes the file - // and not an actual constraint on what should be in these 8 bytes. + // The first 0x10 bytes in the data segment appear to be unused. In most files I've seen, the last half of it (8 + // bytes) are duplicates of the first 8 bytes of the unscrambled, compressed data, though this is the result of + // an uninitialized memory bug when the client encodes the file and not an actual constraint on what should be in + // these 8 bytes. r.skip(16); - // The game treats this field as a 16-byte string (including the \0). The 8 - // bytes after it appear to be completely unused. + // The game treats this field as a 16-byte string (including the \0). The 8 bytes after it appear to be + // completely unused. if (r.readx(15) != "SONICTEAM,SEGA.") { throw runtime_error("Episode 3 GCI file is not a quest"); } @@ -1025,9 +990,8 @@ string decode_gci_data( string decrypted = r.readx(header.data_size - 40); - // For some reason, Sega decided not to encrypt Episode 3 quest files in the - // same way as Episodes 1&2 quest files (see above). Instead, they just - // wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the + // For some reason, Sega decided not to encrypt Episode 3 quest files in the same way as Episodes 1&2 quest files + // (see above). Instead, they just wrote a fairly trivial XOR loop over the first 0x100 bytes, leaving the // remaining bytes completely unencrypted (but still compressed). size_t unscramble_size = min(0x100, data.size()); decrypt_trivial_gci_data(decrypted.data(), unscramble_size, 0); @@ -1046,11 +1010,7 @@ string decode_gci_data( } } -string decode_vms_data( - const string& data, - ssize_t find_seed_num_threads, - int64_t known_seed, - bool skip_checksum) { +string decode_vms_data(const string& data, ssize_t find_seed_num_threads, int64_t known_seed, bool skip_checksum) { phosg::StringReader r(data); const auto& header = r.get(); if (!header.checksum_correct()) { @@ -1065,8 +1025,7 @@ string decode_vms_data( } if (known_seed >= 0) { - return decrypt_download_quest_data_section( - data_section, header.data_size, known_seed); + return decrypt_download_quest_data_section(data_section, header.data_size, known_seed); } else { if (find_seed_num_threads < 0) { @@ -1085,10 +1044,9 @@ string decode_dlq_data(const string& data) { uint32_t decompressed_size = r.get_u32l(); uint32_t key = r.get_u32l(); - // The compressed data size does not need to be a multiple of 4, but the V2 - // encryption (which is used for all download quests, even in V3) requires the - // data size to be a multiple of 4. We'll just temporarily stick a few bytes - // on the end, then throw them away later if needed. + // The compressed data size does not need to be a multiple of 4, but the V2 encryption (which is used for all + // download quests, even in V3) requires the data size to be a multiple of 4. We'll just temporarily stick a few + // bytes on the end, then throw them away later if needed. string decrypted = r.read(r.remaining()); PSOV2Encryption encr(key); size_t original_size = data.size(); @@ -1150,9 +1108,8 @@ static unordered_map decode_qst_data_t(const string& data) { } } else if (header.command == 0x13 || header.command == 0xA7) { - // We have to allow larger commands here, because it seems some tools - // encoded QST files with BB's extra 4 padding bytes included in the - // command size. + // We have to allow larger commands here, because it seems some tools encoded QST files with BB's extra 4 padding + // bytes included in the command size. if (header.size < sizeof(HeaderT) + sizeof(S_WriteFile_13_A7)) { throw runtime_error("qst write file command has incorrect size"); } @@ -1195,9 +1152,8 @@ static unordered_map decode_qst_data_t(const string& data) { } unordered_map decode_qst_data(const string& data) { - // QST files start with an open file command, but the format differs depending - // on the PSO version that the qst file is for. We can detect the format from - // the first 4 bytes in the file: + // QST files start with an open file command, but the format differs depending on the PSO version that the qst file + // is for. We can detect the format from the first 4 bytes in the file: // - BB: 58 00 44 00 or 58 00 A6 00 // - PC: 3C 00 44 ?? or 3C 00 A6 ?? // - DC/GC: 44 ?? 3C 00 or A6 ?? 3C 00 @@ -1209,10 +1165,9 @@ unordered_map decode_qst_data(const string& data) { } else if (((signature & 0xFFFFFF00) == 0x3C004400) || ((signature & 0xFFFFFF00) == 0x3C00A600)) { return decode_qst_data_t(data); } else if (((signature & 0xFF00FFFF) == 0x44003C00) || ((signature & 0xFF00FFFF) == 0xA6003C00)) { - // In PSO DC, the type field is only one byte, but in V3 it's two bytes and - // the filename was shifted over by one byte. To detect this, we check if - // the V3 type field has a reasonable value, and if not, we assume the file - // is for PSO DC. + // In PSO DC, the type field is only one byte, but in V3 it's two bytes and the filename was shifted over by one + // byte. To detect this, we check if the V3 type field has a reasonable value, and if not, we assume the file is + // for PSO DC. if (r.pget_u16l(sizeof(PSOCommandHeaderDCV3) + offsetof(S_OpenFile_PC_GC_44_A6, type)) > 3) { return decode_qst_data_t(data); } else { @@ -1249,8 +1204,7 @@ void add_open_file_command_t( cmd.filename.encode(filename); cmd.type = 0; cmd.file_size = file_size; - // TODO: It'd be nice to have something like w.emplace(...) to avoid copying - // the command structs into the StringWriter. + // TODO: It'd be nice to have something like w.emplace(...) to avoid copying the structs into the StringWriter. w.put(cmd); } @@ -1289,9 +1243,8 @@ void add_write_file_commands_t( memcpy(cmd.data.data(), &data[z], chunk_size); cmd.data_size = chunk_size; w.put(cmd); - // On BB, the write file command size is a multiple of 4 but not a multiple - // of 8; in QST format the implicit extra 4 bytes are apparently stored in - // the file. + // On BB, the write file command size is a multiple of 4 but not a multiple of 8; in QST format the implicit extra + // 4 bytes are apparently stored in the file. if (bb_alignment) { w.put_u32(0); } @@ -1307,15 +1260,15 @@ string encode_qst_file( bool is_dlq_encoded) { phosg::StringWriter w; - // Some tools expect both open file commands at the beginning, hence this - // unfortunate abstraction-breaking. + // Some tools expect both open file commands at the beginning, hence this unfortunate abstraction-breaking. switch (version) { case Version::DC_NTE: // DC NTE doesn't support quests, but we support encoding QST files anyway case Version::DC_11_2000: case Version::DC_V1: case Version::DC_V2: for (const auto& it : files) { - add_open_file_command_t(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); + add_open_file_command_t( + w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); } for (const auto& it : files) { add_write_file_commands_t(w, it.first, *it.second, is_dlq_encoded, false); @@ -1324,7 +1277,8 @@ string encode_qst_file( case Version::PC_NTE: case Version::PC_V2: for (const auto& it : files) { - add_open_file_command_t(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); + add_open_file_command_t( + w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); } for (const auto& it : files) { add_write_file_commands_t(w, it.first, *it.second, is_dlq_encoded, false); @@ -1335,7 +1289,8 @@ string encode_qst_file( case Version::GC_EP3_NTE: case Version::GC_EP3: for (const auto& it : files) { - add_open_file_command_t(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); + add_open_file_command_t( + w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); } for (const auto& it : files) { add_write_file_commands_t(w, it.first, *it.second, is_dlq_encoded, false); @@ -1343,7 +1298,8 @@ string encode_qst_file( break; case Version::XB_V3: for (const auto& it : files) { - add_open_file_command_t(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); + add_open_file_command_t( + w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); } for (const auto& it : files) { add_write_file_commands_t(w, it.first, *it.second, is_dlq_encoded, false); @@ -1351,7 +1307,8 @@ string encode_qst_file( break; case Version::BB_V4: for (const auto& it : files) { - add_open_file_command_t(w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); + add_open_file_command_t( + w, name, it.first, xb_filename, quest_number, it.second->size(), is_dlq_encoded); } for (const auto& it : files) { add_write_file_commands_t(w, it.first, *it.second, is_dlq_encoded, true); diff --git a/src/Quest.hh b/src/Quest.hh index 709dee56..027f37a5 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -69,8 +69,8 @@ struct QuestCategoryIndex { struct VersionedQuest { QuestMetadata meta; - // Most of these default values are intentionally invalid; we use these - // values to check if each field was parsed during quest indexing. + // Most of these default values are intentionally invalid; we use these values to check if each field was parsed + // during quest indexing. std::shared_ptr bin_contents; std::shared_ptr dat_contents; std::shared_ptr map_file; @@ -151,20 +151,12 @@ struct QuestIndex { }; std::string encode_download_quest_data( - const std::string& compressed_data, - size_t decompressed_size = 0, - uint32_t encryption_seed = 0); + const std::string& compressed_data, size_t decompressed_size = 0, uint32_t encryption_seed = 0); std::string decode_gci_data( - const std::string& data, - ssize_t find_seed_num_threads = -1, - int64_t known_seed = -1, - bool skip_checksum = false); + const std::string& data, ssize_t find_seed_num_threads = -1, int64_t known_seed = -1, bool skip_checksum = false); std::string decode_vms_data( - const std::string& data, - ssize_t find_seed_num_threads = -1, - int64_t known_seed = -1, - bool skip_checksum = false); + const std::string& data, ssize_t find_seed_num_threads = -1, int64_t known_seed = -1, bool skip_checksum = false); std::string decode_dlq_data(const std::string& data); std::unordered_map decode_qst_data(const std::string& data); diff --git a/src/QuestMetadata.cc b/src/QuestMetadata.cc index 6ce83484..f6afd17e 100644 --- a/src/QuestMetadata.cc +++ b/src/QuestMetadata.cc @@ -13,7 +13,8 @@ phosg::JSON QuestMetadata::FloorAssignment::json() const { } std::string QuestMetadata::FloorAssignment::str() const { - return std::format("FloorAssignment(floor=0x{:02X}, area=0x{:02X}, type=0x{:02X}, layout_var=0x{:02X}, entities_var=0x{:02X})", + return std::format( + "FloorAssignment(floor=0x{:02X}, area=0x{:02X}, type=0x{:02X}, layout_var=0x{:02X}, entities_var=0x{:02X})", this->floor, this->area, this->type, this->layout_var, this->entities_var); } @@ -82,8 +83,7 @@ void QuestMetadata::assign_default_floors() { void QuestMetadata::assert_compatible(const QuestMetadata& other) const { if (this->quest_number != other.quest_number) { throw logic_error(std::format( - "incorrect versioned quest number (existing: {:08X}, new: {:08X})", - this->quest_number, other.quest_number)); + "incorrect versioned quest number (existing: {:08X}, new: {:08X})", this->quest_number, other.quest_number)); } if (this->category_id != other.category_id) { throw runtime_error(std::format( @@ -98,7 +98,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { if (this->allow_start_from_chat_command != other.allow_start_from_chat_command) { throw runtime_error(std::format( "quest version has a different allow_start_from_chat_command state (existing: {}, new: {})", - this->allow_start_from_chat_command ? "true" : "false", other.allow_start_from_chat_command ? "true" : "false")); + this->allow_start_from_chat_command ? "true" : "false", + other.allow_start_from_chat_command ? "true" : "false")); } if (this->joinable != other.joinable) { throw runtime_error(std::format( @@ -109,7 +110,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { bool other_has_player_limit = (other.max_players != 0) && (other.max_players != 4); if ((this_has_player_limit || other_has_player_limit) && (this->max_players != other.max_players)) { throw runtime_error(std::format( - "quest version has a different maximum player count (existing: {}, new: {})", this->max_players, other.max_players)); + "quest version has a different maximum player count (existing: {}, new: {})", + this->max_players, other.max_players)); } if (this->lock_status_register != other.lock_status_register) { throw runtime_error(std::format( @@ -122,8 +124,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { if (this->solo_unlock_flags != other.solo_unlock_flags) { throw runtime_error(std::format("quest version has a different set of solo unlock flags")); } - if (!this->create_item_mask_entries.empty() && - !other.create_item_mask_entries.empty() && + if (!this->create_item_mask_entries.empty() && !other.create_item_mask_entries.empty() && this->create_item_mask_entries != other.create_item_mask_entries) { string this_str, other_str; for (const auto& item : this->create_item_mask_entries) { @@ -150,8 +151,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { string existing_str = this->battle_rules->json().serialize(); string new_str = other.battle_rules->json().serialize(); throw runtime_error(std::format( - "quest version has different battle rules (existing: {}, new: {})", - existing_str, new_str)); + "quest version has different battle rules (existing: {}, new: {})", existing_str, new_str)); } if (this->challenge_template_index != other.challenge_template_index) { throw runtime_error(std::format( @@ -173,7 +173,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { const auto& other_fa = other.floor_assignments[z]; if (this_fa.area != other_fa.area) { throw runtime_error(std::format( - "quest version has different area on floor 0x{:02X} (existing: {}, new: {})", z, this_fa.str(), other_fa.str())); + "quest version has different area on floor 0x{:02X} (existing: {}, new: {})", + z, this_fa.str(), other_fa.str())); } } if (this->description_flag != other.description_flag) { @@ -190,8 +191,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { string existing_str = this->available_expression->str(); string new_str = other.available_expression->str(); throw runtime_error(std::format( - "quest version has a different available expression (existing: {}, new: {})", - existing_str, new_str)); + "quest version has a different available expression (existing: {}, new: {})", existing_str, new_str)); } if (!this->enabled_expression != !other.enabled_expression) { throw runtime_error(std::format( @@ -202,8 +202,7 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { string existing_str = this->enabled_expression->str(); string new_str = other.enabled_expression->str(); throw runtime_error(std::format( - "quest version has a different enabled expression (existing: {}, new: {})", - existing_str, new_str)); + "quest version has a different enabled expression (existing: {}, new: {})", existing_str, new_str)); } if (this->common_item_set_name != other.common_item_set_name) { throw runtime_error(std::format( @@ -224,7 +223,8 @@ void QuestMetadata::assert_compatible(const QuestMetadata& other) const { phosg::name_for_enum(this->default_drop_mode), phosg::name_for_enum(other.default_drop_mode))); } if (this->enable_schtserv_commands != other.enable_schtserv_commands) { - throw runtime_error(format("quest version has different value for enable_schtserv_commands (existing: {}, new: {})", + throw runtime_error(format( + "quest version has different value for enable_schtserv_commands (existing: {}, new: {})", this->enable_schtserv_commands ? "true" : "false", other.enable_schtserv_commands ? "true" : "false")); } } @@ -239,7 +239,8 @@ phosg::JSON QuestMetadata::json() const { auto difficulty = static_cast((key >> 24) & 3); auto floor = static_cast((key >> 16) & 0xFF); auto enemy_type = static_cast(key & 0xFFFF); - auto key_str = std::format("{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type)); + auto key_str = std::format( + "{}:0x{:02X}:{}", name_for_difficulty(difficulty), floor, phosg::name_for_enum(enemy_type)); enemy_exp_overrides_json.emplace(key_str, exp_override); } diff --git a/src/QuestMetadata.hh b/src/QuestMetadata.hh index 167c5bf7..df1635d4 100644 --- a/src/QuestMetadata.hh +++ b/src/QuestMetadata.hh @@ -17,10 +17,9 @@ #include "StaticGameData.hh" struct QuestMetadata { - // This structure contains configuration that should be the same across all - // versions of the quest, except for the name and description strings. This - // is used in both the Quest and VersionedQuest structures; in Quest, the - // name and description are used only internally. + // This structure contains configuration that should be the same across all versions of the quest, except for the + // name and description strings. This is used in both the Quest and VersionedQuest structures; in Quest, the name and + // description are used only internally. Version version; Language language; diff --git a/src/QuestScript.cc b/src/QuestScript.cc index cd34ac01..934d32c4 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -26,37 +26,26 @@ using namespace std; // This file documents PSO's quest script execution system. // The quest execution system has several relevant data structures: -// - The quest script is a stream of binary data containing opcodes (as defined -// below), each followed by their arguments. The offset of the code section -// of this stream is defined here. -// - The execution state specifies what the client should do on every frame. -// There are many possible states here, such as waiting for the player to -// dismiss a chat bubble, choose an item from a menu, etc. -// - The function table is a list of offsets into the quest script which can be -// used as targets for jumps and calls, as well as references to large data -// structures that don't fit in quest opcode arguments. -// - The quest registers are 32-bit integers referred to as r0-r255. In later -// versions, registers may contain floating-point values, in which case -// they're referred to as f0-f255 (but they still occupy the same memory as -// r0-255). -// - The args list is a list of up to 8 32-bit values used for many quest -// opcodes in v3 and later. These opcodes are preceded by one or more -// arg_push opcodes, which allow scripts the ability to pass values from -// immediate data, registers, labels, or even pointers to registers. Opcodes -// that use the args list are tagged with F_ARGS below. -// - The stack is an array of 32-bit integers (16 of them on v1/v2, 64 of them -// on v3/v4), which is used by the call and ret opcodes (which push and pop -// offsets into the quest script), but may also be used by the stack_push and -// stack_pop opcodes to work with arbitrary data. There is protection from -// stack underflows (the caller receives the value 0, or the thread -// terminates in case of the ret opcode), but there is no protection from -// overflows. -// - The quest flags are a per-character array of 1024 single-bit flags saved -// with the character data. (On Episode 3, there are 8192 instead.) -// - The quest counters are a per-character array of 16 32-bit values saved -// with the character data. (On Episode 3, there are 48 instead.) -// - The event flags are an array of 0x100 bytes stored in the system file (not -// the character file). +// - The quest script is a stream of binary data containing opcodes (as defined below), each followed by their +// arguments. The offset of the code section of this stream is defined here. +// - The execution state specifies what the client should do on every frame. There are many possible states here, such +// as waiting for the player to dismiss a chat bubble, choose an item from a menu, etc. +// - The function table is a list of offsets into the quest script which can be used as targets for jumps and calls, as +// well as references to large data structures that don't fit in quest opcode arguments. +// - The quest registers are 32-bit integers referred to as r0-r255. In later versions, registers may contain floating- +// point values, in which case they're referred to as f0-f255 (but they still occupy the same memory as r0-255). +// - The args list is a list of up to 8 32-bit values used for many quest opcodes in v3 and later. These opcodes are +// preceded by one or more arg_push opcodes, which allow scripts the ability to pass values from immediate data, +// registers, labels, or even pointers to registers. Opcodes that use the args list are tagged with F_ARGS below. +// - The stack is an array of 32-bit integers (16 of them on v1/v2, 64 of them on v3/v4), which is used by the call and +// ret opcodes (which push and pop offsets into the quest script), but may also be used by the stack_push and +// stack_pop opcodes to work with arbitrary data. There is protection from stack underflows (the caller receives the +// value 0, or the thread terminates in case of the ret opcode), but there is no protection from overflows. +// - The quest flags are a per-character array of 1024 single-bit flags saved with the character data. (On Episode 3, +// there are 8192 instead.) +// - The quest counters are a per-character array of 16 32-bit values saved with the character data. (On Episode 3, +// there are 48 instead.) +// - The event flags are an array of 0x100 bytes stored in the system file (not the character file). using AttackData = BattleParamsIndex::AttackData; using ResistData = BattleParamsIndex::ResistData; @@ -182,15 +171,9 @@ struct QuestScriptOpcodeDefinition { const char* name; Argument(Type type, size_t count = 0, const char* name = nullptr) - : type(type), - count(count), - data_type(DataType::NONE), - name(name) {} + : type(type), count(count), data_type(DataType::NONE), name(name) {} Argument(Type type, DataType data_type, const char* name = nullptr) - : type(type), - count(0), - data_type(data_type), - name(name) {} + : type(type), count(0), data_type(data_type), name(name) {} }; uint16_t opcode; @@ -200,16 +183,8 @@ struct QuestScriptOpcodeDefinition { uint32_t flags; QuestScriptOpcodeDefinition( - uint16_t opcode, - const char* name, - const char* qedit_name, - std::vector args, - uint32_t flags) - : opcode(opcode), - name(name), - qedit_name(qedit_name), - args(args), - flags(flags) {} + uint16_t opcode, const char* name, const char* qedit_name, std::vector args, uint32_t flags) + : opcode(opcode), name(name), qedit_name(qedit_name), args(args), flags(flags) {} std::string str() const { string name_str = this->qedit_name ? std::format("{} (qedit: {})", this->name, this->qedit_name) : this->name; @@ -227,23 +202,22 @@ static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the QuestScript flags static constexpr uint32_t F_PUSH_ARG = 0x00010000; static constexpr uint32_t F_CLEAR_ARGS = 0x00020000; -// F_ARGS means this opcode takes its arguments via the argument list on v3 and -// later. It has no effect on v2 and earlier. +// F_ARGS means this opcode uses the argument list on v3 and later; it has no effect on v2 and earlier static constexpr uint32_t F_ARGS = 0x00040000; static constexpr uint32_t F_TERMINATOR = 0x00080000; -// The following flags are used to specify which versions support each opcode. -static constexpr uint32_t F_DC_NTE = 0x0004; // Version::DC_NTE -static constexpr uint32_t F_DC_112000 = 0x0008; // Version::DC_11_2000 -static constexpr uint32_t F_DC_V1 = 0x0010; // Version::DC_V1 -static constexpr uint32_t F_DC_V2 = 0x0020; // Version::DC_V2 -static constexpr uint32_t F_PC_NTE = 0x0040; // Version::PC_NTE -static constexpr uint32_t F_PC_V2 = 0x0080; // Version::PC_V2 -static constexpr uint32_t F_GC_NTE = 0x0100; // Version::GC_NTE -static constexpr uint32_t F_GC_V3 = 0x0200; // Version::GC_V3 -static constexpr uint32_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_NTE -static constexpr uint32_t F_GC_EP3 = 0x0800; // Version::GC_EP3 -static constexpr uint32_t F_XB_V3 = 0x1000; // Version::XB_V3 -static constexpr uint32_t F_BB_V4 = 0x2000; // Version::BB_V4 +// The following flags are used to specify which versions support each opcode +static constexpr uint32_t F_DC_NTE = 0x00000004; // Version::DC_NTE +static constexpr uint32_t F_DC_112000 = 0x00000008; // Version::DC_11_2000 +static constexpr uint32_t F_DC_V1 = 0x00000010; // Version::DC_V1 +static constexpr uint32_t F_DC_V2 = 0x00000020; // Version::DC_V2 +static constexpr uint32_t F_PC_NTE = 0x00000040; // Version::PC_NTE +static constexpr uint32_t F_PC_V2 = 0x00000080; // Version::PC_V2 +static constexpr uint32_t F_GC_NTE = 0x00000100; // Version::GC_NTE +static constexpr uint32_t F_GC_V3 = 0x00000200; // Version::GC_V3 +static constexpr uint32_t F_GC_EP3TE = 0x00000400; // Version::GC_EP3_NTE +static constexpr uint32_t F_GC_EP3 = 0x00000800; // Version::GC_EP3 +static constexpr uint32_t F_XB_V3 = 0x00001000; // Version::XB_V3 +static constexpr uint32_t F_BB_V4 = 0x00002000; // Version::BB_V4 static_assert(F_DC_NTE == v_flag(Version::DC_NTE)); static_assert(F_DC_112000 == v_flag(Version::DC_11_2000)); @@ -275,39 +249,34 @@ static constexpr uint32_t F_V4 = // clang-format on static constexpr uint32_t F_HAS_ARGS = F_V3_V4; -// These are the argument data types. All values are stored little-endian in -// the script data, even on the GameCube. +// These are the argument data types. All values are stored little-endian in the script data, even on the GameCube. // LABEL16 is a 16-bit index into the function table static constexpr auto LABEL16 = Arg::Type::LABEL16; -// LABEL16_SET is a single byte specifying how many labels follow, followed by -// that many 16-bit indexes into the function table. +// LABEL16_SET is a single byte specifying how many labels follow, followed by that many 16-bit indexes into the +// function table. static constexpr auto LABEL16_SET = Arg::Type::LABEL16_SET; // LABEL32 is a 32-bit index into the function table static constexpr auto LABEL32 = Arg::Type::LABEL32; -// R_REG is a single byte specifying a register number (rXX or fXX) which is -// read by the opcode and not modified +// R_REG is a single byte specifying a register number (rXX or fXX) which is read by the opcode and not modified static constexpr auto R_REG = Arg::Type::R_REG; -// W_REG is a single byte specifying a register number (rXX or fXX) which is -// written by the opcode (and maybe also read beforehand) +// W_REG is a single byte specifying a register number (rXX or fXX) which is written by the opcode (and maybe also read +// beforehand) static constexpr auto W_REG = Arg::Type::W_REG; -// R_REG_SET is a single byte specifying how many registers follow, followed by -// that many bytes specifying individual register numbers. +// R_REG_SET is a single byte specifying how many registers follow, followed by that many bytes specifying individual +// register numbers. static constexpr auto R_REG_SET = Arg::Type::R_REG_SET; -// R_REG_SET_FIXED is a single byte specifying a register number, but the -// opcode implicitly reads the following registers as well. For example, if an -// opcode takes a {REG_SET_FIXED, 4} and the value 100 was passed to that -// opcode, only the byte 0x64 would appear in the script data, but the opcode -// would use r100, r101, r102, and r103. +// R_REG_SET_FIXED is a single byte specifying a register number, but the opcode implicitly reads the following +// registers as well. For example, if an opcode takes a {REG_SET_FIXED, 4} and the value 100 was passed to that opcode, +// only the byte 0x64 would appear in the script data, but the opcode would use r100, r101, r102, and r103. static constexpr auto R_REG_SET_FIXED = Arg::Type::R_REG_SET_FIXED; -// W_REG_SET_FIXED is like R_REG_SET_FIXED, but is used for registers that are -// written (and maybe read beforehand) by the opcode. +// W_REG_SET_FIXED is like R_REG_SET_FIXED, but is used for registers that are written (and maybe read beforehand) by +// the opcode. static constexpr auto W_REG_SET_FIXED = Arg::Type::W_REG_SET_FIXED; // [R/W]_REG32 is a 32-bit register number. The high 24 bits are unused. static constexpr auto R_REG32 = Arg::Type::R_REG32; static constexpr auto W_REG32 = Arg::Type::W_REG32; -// [RW]_REG32_SET_FIXED is like [RW]_REG_SET_FIXED, but uses a 32-bit register -// number. The high 24 bits are unused. +// [RW]_REG32_SET_FIXED is like [RW]_REG_SET_FIXED, but uses a 32-bit register number. The high 24 bits are unused. static constexpr auto R_REG32_SET_FIXED = Arg::Type::R_REG32_SET_FIXED; static constexpr auto W_REG32_SET_FIXED = Arg::Type::W_REG32_SET_FIXED; // I8, I16, and I32 are unsigned integers of various sizes @@ -319,8 +288,8 @@ static constexpr auto FLOAT32 = Arg::Type::FLOAT32; // CSTRING is a sequence of nonzero bytes ending with a zero byte static constexpr auto CSTRING = Arg::Type::CSTRING; -// These are shortcuts for the above types with some extra metadata, which the -// disassembler uses to annotate arguments or data sections. +// These are shortcuts for the above types with some extra metadata, which the disassembler uses to annotate arguments +// or data sections. static const Arg SCRIPT16(LABEL16, Arg::DataType::SCRIPT); static const Arg SCRIPT16_SET(LABEL16_SET, Arg::DataType::SCRIPT); static const Arg SCRIPT32(LABEL32, Arg::DataType::SCRIPT); @@ -332,17 +301,13 @@ static const Arg ITEM_ID(I32, 0, "item_id"); static const Arg FLOOR(I32, 0, "floor"); static const QuestScriptOpcodeDefinition opcode_defs[] = { - // The quest opcodes are defined below. Two-byte opcodes begin with F8 or - // F9; all other opcodes are one byte. Unlike network commands and - // subcommands, all versions use the same values for almost all opcodes - // (there is one exception), but not all opcodes are supported on all - // versions. The flags denote which versions support each opcode; opcodes - // are defined multiple times below if their call signatures are different - // across versions. + // The quest opcodes are defined below. Two-byte opcodes begin with F8 or F9; all other opcodes are one byte. + // Unlike network commands and subcommands, all versions use the same values for almost all opcodes (there is one + // exception), but not all opcodes are supported on all versions. The flags denote which versions support each + // opcode; opcodes are defined multiple times below if their call signatures are different across versions. - // In the comments below, arguments are referred to with letters. The first - // argument to an opcode would be regA (if it's a REG), the second is regB, - // etc. The individual registers within a REG_SET_FIXED argument are + // In the comments below, arguments are referred to with letters. The first argument to an opcode would be regA (if + // it's a W_REG or R_REG), the second is regB, etc. The individual registers within a REG_SET_FIXED argument are // referred to as an array, as regsA[0], regsA[1], etc. // Does nothing @@ -351,8 +316,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Pops new PC off stack {0x01, "ret", nullptr, {}, F_V0_V4 | F_TERMINATOR}, - // Stops execution for the current frame. Execution resumes immediately - // after this opcode on the next frame. + // Stops execution for the current frame. Execution resumes immediately after this opcode on the next frame. {0x02, "sync", nullptr, {}, F_V0_V4}, // Exits entirely @@ -367,9 +331,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Pops r7-r1 from the stack {0x06, "va_end", nullptr, {}, F_V3_V4}, - // Replaces r1-r7 with the args list, then calls labelA - // Note: This opcode doesn't directly clear the args list, but we assume - // during disassembly that the code being called does so. + // Replaces r1-r7 with the args list, then calls labelA. This opcode doesn't directly clear the args list, but we + // assume during disassembly that the code being called does so. {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4 | F_CLEAR_ARGS}, // Copies a value from regB to regA @@ -378,8 +341,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets regA to valueB {0x09, "leti", nullptr, {W_REG, I32}, F_V0_V4}, - // Sets regA to the memory address of regB. Note that this opcode was moved - // to 0C in v3 and later. + // Sets regA to the memory address of regB. Note that this opcode was moved to 0C in v3 and later. {0x0A, "leta", nullptr, {W_REG, R_REG}, F_V0_V2}, // Sets regA to valueB @@ -391,8 +353,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets regA to the memory address of regB {0x0C, "leta", nullptr, {W_REG, R_REG}, F_V3_V4}, - // Sets regA to the address of the offset of labelB in the function table - // (to get the offset, use read4 after this) + // Sets regA to the address of the offset of labelB in the function table (to get the offset, use read4 after this) {0x0D, "leto", nullptr, {W_REG, LABEL16}, F_V3_V4}, // Sets regA to 1 @@ -407,13 +368,11 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets flagA to 1. Sends 6x75. {0x13, "gset", nullptr, {I16}, F_V0_V4}, - // Clears flagA to 0. Sends 6x75 on BB, but does not send anything on other - // versions. + // Clears flagA to 0. Sends 6x75 on BB, but does not send anything on other versions. {0x14, "gclear", nullptr, {I16}, F_V0_V4}, - // Inverts flagA. Like the above two opcodes, sends 6x75 if the flag is set - // by this opcode. Only BB sends 6x75 if the flag is cleared by this - // opcode. + // Inverts flagA. Like the above two opcodes, sends 6x75 if the flag is set by this opcode. Only BB sends 6x75 if + // the flag is cleared by this opcode. {0x15, "grev", nullptr, {I16}, F_V0_V4}, // If regB is nonzero, sets flagA; otherwise, clears it @@ -465,8 +424,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x25, "xori", nullptr, {W_REG, I32}, F_V0_V4}, // regA %= regB - // Note: This does signed division, so if the value is negative, you might - // get unexpected results. + // Note: This does signed division, so if the value is negative, you might get unexpected results. {0x26, "mod", nullptr, {W_REG, R_REG}, F_V3_V4}, // regA %= valueB @@ -476,10 +434,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Jumps to labelA {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4 | F_TERMINATOR}, - // Pushes the script offset immediately after this opcode and jumps to - // labelA - // Note: This opcode doesn't directly clear the args list, but we assume - // during disassembly that the code being called does so. + // Pushes the script offset immediately after this opcode and jumps to labelA + // Note: This opcode doesn't directly clear the args list, but we assume during disassembly that the code being + // called does so. {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4 | F_CLEAR_ARGS}, // If all values in regsB are nonzero, jumps to labelA @@ -552,8 +509,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x40, "switch_jmp", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4}, // Calls labelsB[regA]; if regA is out of range of labelsB, does nothing - // Note: This opcode doesn't directly clear the args list, but we assume - // during disassembly that the code being called does so. + // Note: This opcode doesn't directly clear the args list, but we assume during disassembly that the code being + // called does so. {0x41, "switch_call", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4 | F_CLEAR_ARGS}, // Does nothing @@ -591,8 +548,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Creates dialogue with an object/NPC (valueA) starting with message strB {0x50, "message", nullptr, {I32, CSTRING}, F_V0_V4 | F_ARGS}, - // Prompts the player with a list of choices (strB; items separated by - // newlines) and returns the index of their choice in regA + // Prompts the player with a list of choices (strB; items separated by newlines) and returns the index of their + // choice in regA {0x51, "list", nullptr, {W_REG, CSTRING}, F_V0_V4 | F_ARGS}, // Fades from black @@ -604,9 +561,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Plays a sound effect {0x54, "sound_effect", "se", {I32}, F_V0_V4 | F_ARGS}, - // Plays a fanfare (clear.adx if valueA is 0, or miniclear.adx if it's 1). - // Note: There is no bounds check on this; values other than 0 or 1 will - // result in undefined behavior. + // Plays a fanfare (clear.adx if valueA is 0, or miniclear.adx if it's 1). There is no bounds check on this; values + // other than 0 or 1 will result in undefined behavior. {0x55, "bgm", nullptr, {I32}, F_V0_V4 | F_ARGS}, // Does nothing @@ -615,31 +571,27 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x58, "nop_58", "enable", {I32}, F_V0_V2}, {0x59, "nop_59", "disable", {I32}, F_V0_V2}, - // Displays a message. Special tokens are interpolated within the string. - // These special tokens are: - // => value of rXX as %d (signed integer) - // => value of rXX as %f (floating-point) (v3 and later) - // => changes text color like $CX would (supported on 11/2000 and - // later); X must be numeric and in the range 0-7, so , , and do not work (though \tC8, \tC9, and \tCG can be used - // directly in the text, and do work) - // => newline - // or => character's name - // or => character's class - //