From 6b274186c5fdd08f0401bb832c7ab41f69bc8f4b Mon Sep 17 00:00:00 2001 From: server Date: Tue, 14 Apr 2026 12:33:08 +0200 Subject: [PATCH] tests: exercise mall open in smoke login --- tests/login_smoke.cpp | 102 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/tests/login_smoke.cpp b/tests/login_smoke.cpp index 3f727b3..8b2fee8 100644 --- a/tests/login_smoke.cpp +++ b/tests/login_smoke.cpp @@ -27,6 +27,8 @@ constexpr size_t ACCOUNT_STATUS_MAX_LEN_LOCAL = 8; constexpr size_t PLAYER_PER_ACCOUNT_LOCAL = 4; constexpr size_t CHARACTER_NAME_MAX_LEN_LOCAL = 64; constexpr size_t GUILD_NAME_MAX_LEN_LOCAL = 12; +constexpr size_t SAFEBOX_PASSWORD_MAX_LEN_LOCAL = 6; +constexpr uint8_t CHAT_TYPE_TALKING_LOCAL = 0; #pragma pack(push, 1) struct PacketGCPhase @@ -77,6 +79,13 @@ struct PacketCGLogin2 uint32_t login_key; }; +struct PacketCGChat +{ + uint16_t header; + uint16_t length; + uint8_t type; +}; + struct SimplePlayerLocal { uint32_t id; @@ -216,6 +225,19 @@ struct PacketGCChannel uint16_t length; uint8_t channel; }; + +struct PacketGCSafeboxSize +{ + uint16_t header; + uint16_t length; + uint8_t size; +}; + +struct PacketGCSafeboxWrongPassword +{ + uint16_t header; + uint16_t length; +}; #pragma pack(pop) struct SmokeOptions @@ -225,6 +247,7 @@ struct SmokeOptions std::string client_version = "1215955205"; std::string expect_auth_failure; std::string expect_channel_failure; + std::string mall_password; }; struct SmokeResult @@ -240,6 +263,8 @@ struct SmokeResult std::string character_name; bool character_created = false; int game_channel = -1; + bool mall_opened = false; + int mall_size = -1; int64_t auth_handshake_ms = -1; int64_t auth_login_ms = -1; int64_t channel_handshake_ms = -1; @@ -247,6 +272,7 @@ struct SmokeResult int64_t character_create_ms = -1; int64_t character_select_ms = -1; int64_t entergame_ms = -1; + int64_t mall_open_ms = -1; std::vector events; }; @@ -333,6 +359,8 @@ void PrintJson(const SmokeResult& result) << ",\"character_name\":\"" << JsonEscape(result.character_name) << "\"" << ",\"character_created\":" << (result.character_created ? "true" : "false") << ",\"game_channel\":" << result.game_channel + << ",\"mall_opened\":" << (result.mall_opened ? "true" : "false") + << ",\"mall_size\":" << result.mall_size << ",\"timings_ms\":{" << "\"auth_handshake\":" << result.auth_handshake_ms << ",\"auth_login\":" << result.auth_login_ms @@ -341,6 +369,7 @@ void PrintJson(const SmokeResult& result) << ",\"character_create\":" << result.character_create_ms << ",\"character_select\":" << result.character_select_ms << ",\"entergame\":" << result.entergame_ms + << ",\"mall_open\":" << result.mall_open_ms << "},\"events\":["; for (size_t i = 0; i < result.events.size(); ++i) @@ -470,6 +499,14 @@ public: WriteExact(m_fd, &encrypted, sizeof(encrypted), "write encrypted packet"); } + void SendEncryptedFrame(const void* data, size_t length) + { + std::vector encrypted(length); + std::memcpy(encrypted.data(), data, length); + m_cipher.EncryptInPlace(encrypted.data(), encrypted.size()); + WriteExact(m_fd, encrypted.data(), encrypted.size(), "write encrypted frame"); + } + bool WaitForFrame(std::vector& frame, int timeout_ms) { while (true) @@ -763,6 +800,17 @@ void SendClientVersion(EncryptedClient& channel_client, std::string_view client_ EmitEvent(result, json, "client_version " + std::string(version_packet.timestamp)); } +void SendChatCommand(EncryptedClient& channel_client, std::string_view command) +{ + std::vector frame(sizeof(PacketCGChat) + command.size() + 1, 0); + auto* chat = reinterpret_cast(frame.data()); + chat->header = CG::CHAT; + chat->length = static_cast(frame.size()); + chat->type = CHAT_TYPE_TALKING_LOCAL; + std::memcpy(frame.data() + sizeof(PacketCGChat), command.data(), command.size()); + channel_client.SendEncryptedFrame(frame.data(), frame.size()); +} + void EnterGame(EncryptedClient& channel_client, SmokeResult& result, bool json) { PacketCGEnterGame enter_game {}; @@ -822,6 +870,41 @@ void EnterGame(EncryptedClient& channel_client, SmokeResult& result, bool json) return; } } + +void OpenMall(EncryptedClient& channel_client, const std::string& password, SmokeResult& result, bool json) +{ + Expect(!password.empty(), "mall password is empty"); + Expect(password.size() <= SAFEBOX_PASSWORD_MAX_LEN_LOCAL, "mall password too long"); + + SendChatCommand(channel_client, "/mall_password " + password); + + std::vector frame; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + + while (true) + { + const int timeout_ms = RemainingTimeoutMs(deadline); + Expect(timeout_ms > 0, "timed out waiting for mall open response"); + Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for mall open response"); + const auto header = FrameHeader(frame); + + if (header == GC::SAFEBOX_WRONG_PASSWORD) + { + Expect(frame.size() == sizeof(PacketGCSafeboxWrongPassword), "unexpected safebox wrong password packet size"); + throw std::runtime_error("mall password rejected"); + } + + if (header == GC::MALL_OPEN) + { + Expect(frame.size() == sizeof(PacketGCSafeboxSize), "unexpected mall open packet size"); + const auto* mall_open = reinterpret_cast(frame.data()); + result.mall_opened = true; + result.mall_size = mall_open->size; + EmitEvent(result, json, "mall_open size=" + std::to_string(static_cast(mall_open->size))); + return; + } + } +} } int main(int argc, char** argv) @@ -835,7 +918,7 @@ int main(int argc, char** argv) "usage: metin_login_smoke \n" " or: metin_login_smoke --password-env=ENV_NAME\n" " optional: --create-character-name=NAME [--client-version=VERSION] [--json] " - "[--expect-auth-failure=STATUS] [--expect-channel-failure=STATUS]"); + "[--expect-auth-failure=STATUS] [--expect-channel-failure=STATUS] [--mall-password=PASSWORD]"); const std::string host = argv[1]; const uint16_t auth_port = static_cast(std::stoi(argv[2])); @@ -863,6 +946,7 @@ int main(int argc, char** argv) const std::string version_prefix = "--client-version="; const std::string auth_failure_prefix = "--expect-auth-failure="; const std::string channel_failure_prefix = "--expect-channel-failure="; + const std::string mall_password_prefix = "--mall-password="; if (create_arg.rfind(create_prefix, 0) == 0) { options.create_character_name = create_arg.substr(create_prefix.size()); @@ -892,6 +976,14 @@ int main(int argc, char** argv) continue; } + if (create_arg.rfind(mall_password_prefix, 0) == 0) + { + options.mall_password = create_arg.substr(mall_password_prefix.size()); + Expect(!options.mall_password.empty(), "mall password is empty"); + Expect(options.mall_password.size() <= SAFEBOX_PASSWORD_MAX_LEN_LOCAL, "mall password too long"); + continue; + } + if (create_arg == "--json") { options.json = true; @@ -1008,6 +1100,14 @@ int main(int argc, char** argv) EnterGame(channel_client, result, options.json); result.entergame_ms = ElapsedMs(started_at); + if (!options.mall_password.empty()) + { + result.stage = "mall_open"; + started_at = std::chrono::steady_clock::now(); + OpenMall(channel_client, options.mall_password, result, options.json); + result.mall_open_ms = ElapsedMs(started_at); + } + result.ok = true; result.result = "success"; result.stage = "complete";