#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/packet_headers.h" #include "game/SecureCipher.h" namespace { constexpr size_t LOGIN_MAX_LEN_LOCAL = 30; constexpr size_t PASSWD_MAX_LEN_LOCAL = 16; 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; #pragma pack(push, 1) struct PacketGCPhase { uint16_t header; uint16_t length; uint8_t phase; }; struct PacketGCKeyChallenge { uint16_t header; uint16_t length; uint8_t server_pk[SecureCipher::PK_SIZE]; uint8_t challenge[SecureCipher::CHALLENGE_SIZE]; uint32_t server_time; }; struct PacketCGKeyResponse { uint16_t header; uint16_t length; uint8_t client_pk[SecureCipher::PK_SIZE]; uint8_t challenge_response[SecureCipher::HMAC_SIZE]; }; struct PacketGCKeyComplete { uint16_t header; uint16_t length; uint8_t encrypted_token[SecureCipher::SESSION_TOKEN_SIZE + SecureCipher::TAG_SIZE]; uint8_t nonce[SecureCipher::NONCE_SIZE]; }; struct PacketCGLogin3 { uint16_t header; uint16_t length; char login[LOGIN_MAX_LEN_LOCAL + 1]; char passwd[PASSWD_MAX_LEN_LOCAL + 1]; }; struct PacketCGLogin2 { uint16_t header; uint16_t length; char login[LOGIN_MAX_LEN_LOCAL + 1]; uint32_t login_key; }; struct SimplePlayerLocal { uint32_t id; char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1]; uint8_t job; uint8_t level; uint32_t play_minutes; uint8_t st; uint8_t ht; uint8_t dx; uint8_t iq; uint16_t main_part; uint8_t change_name; uint16_t hair_part; uint8_t dummy[4]; int32_t x; int32_t y; uint32_t addr; uint16_t port; uint8_t skill_group; }; struct PacketGCLoginSuccess4 { uint16_t header; uint16_t length; SimplePlayerLocal players[PLAYER_PER_ACCOUNT_LOCAL]; uint32_t guild_id[PLAYER_PER_ACCOUNT_LOCAL]; char guild_name[PLAYER_PER_ACCOUNT_LOCAL][GUILD_NAME_MAX_LEN_LOCAL + 1]; uint32_t handle; uint32_t random_key; }; struct PacketGCAuthSuccess { uint16_t header; uint16_t length; uint32_t login_key; uint8_t result; }; struct PacketGCLoginFailure { uint16_t header; uint16_t length; char status[ACCOUNT_STATUS_MAX_LEN_LOCAL + 1]; }; struct PacketGCEmpire { uint16_t header; uint16_t length; uint8_t empire; }; struct PacketCGPlayerCreate { uint16_t header; uint16_t length; uint8_t index; char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1]; uint16_t job; uint8_t shape; uint8_t con; uint8_t intel; uint8_t str; uint8_t dex; }; struct PacketGCPlayerCreateSuccess { uint16_t header; uint16_t length; uint8_t account_character_index; SimplePlayerLocal player; }; struct PacketGCCreateFailure { uint16_t header; uint16_t length; uint8_t type; }; struct PacketCGPlayerSelect { uint16_t header; uint16_t length; uint8_t index; }; struct PacketCGEnterGame { uint16_t header; uint16_t length; }; struct PacketCGClientVersion { uint16_t header; uint16_t length; char filename[32 + 1]; char timestamp[32 + 1]; }; struct PacketGCMainCharacter { enum { MUSIC_NAME_LEN = 24 }; uint16_t header; uint16_t length; uint32_t vid; uint16_t race_num; char name[CHARACTER_NAME_MAX_LEN_LOCAL + 1]; char bgm_name[MUSIC_NAME_LEN + 1]; float bgm_vol; int32_t x; int32_t y; int32_t z; uint8_t empire; uint8_t skill_group; }; struct PacketGCTime { uint16_t header; uint16_t length; time_t time; }; struct PacketGCChannel { uint16_t header; uint16_t length; uint8_t channel; }; #pragma pack(pop) struct SmokeOptions { bool json = false; std::string create_character_name; std::string client_version = "1215955205"; std::string expect_auth_failure; std::string expect_channel_failure; }; struct SmokeResult { bool ok = false; std::string result = "unexpected_failure"; std::string stage = "startup"; std::string error; std::string failure_status; uint32_t login_key = 0; int empire = -1; int character_index = -1; std::string character_name; bool character_created = false; int game_channel = -1; int64_t auth_handshake_ms = -1; int64_t auth_login_ms = -1; int64_t channel_handshake_ms = -1; int64_t channel_login_ms = -1; int64_t character_create_ms = -1; int64_t character_select_ms = -1; int64_t entergame_ms = -1; std::vector events; }; struct AuthResponse { bool success = false; uint32_t login_key = 0; std::string failure_status; }; struct ChannelLoginResponse { bool success = false; int empire = -1; std::string failure_status; PacketGCLoginSuccess4 login_success {}; }; void Expect(bool condition, std::string_view message) { if (!condition) throw std::runtime_error(std::string(message)); } std::string JsonEscape(std::string_view input) { std::string escaped; escaped.reserve(input.size() + 8); for (const char c : input) { switch (c) { case '\\': escaped += "\\\\"; break; case '"': escaped += "\\\""; break; case '\n': escaped += "\\n"; break; case '\r': escaped += "\\r"; break; case '\t': escaped += "\\t"; break; default: escaped.push_back(c); break; } } return escaped; } void EmitEvent(SmokeResult& result, bool json, const std::string& line) { result.events.push_back(line); if (!json) std::cout << line << "\n"; } int64_t ElapsedMs(std::chrono::steady_clock::time_point started_at) { return std::chrono::duration_cast( std::chrono::steady_clock::now() - started_at) .count(); } void PrintJson(const SmokeResult& result) { std::cout << "{" << "\"ok\":" << (result.ok ? "true" : "false") << ",\"result\":\"" << JsonEscape(result.result) << "\"" << ",\"stage\":\"" << JsonEscape(result.stage) << "\"" << ",\"error\":\"" << JsonEscape(result.error) << "\"" << ",\"failure_status\":\"" << JsonEscape(result.failure_status) << "\"" << ",\"login_key\":" << result.login_key << ",\"empire\":" << result.empire << ",\"character_index\":" << result.character_index << ",\"character_name\":\"" << JsonEscape(result.character_name) << "\"" << ",\"character_created\":" << (result.character_created ? "true" : "false") << ",\"game_channel\":" << result.game_channel << ",\"timings_ms\":{" << "\"auth_handshake\":" << result.auth_handshake_ms << ",\"auth_login\":" << result.auth_login_ms << ",\"channel_handshake\":" << result.channel_handshake_ms << ",\"channel_login\":" << result.channel_login_ms << ",\"character_create\":" << result.character_create_ms << ",\"character_select\":" << result.character_select_ms << ",\"entergame\":" << result.entergame_ms << "},\"events\":["; for (size_t i = 0; i < result.events.size(); ++i) { if (i != 0) std::cout << ","; std::cout << "\"" << JsonEscape(result.events[i]) << "\""; } std::cout << "]}\n"; } void WriteExact(int fd, const void* data, size_t length, std::string_view context) { const uint8_t* cursor = static_cast(data); size_t remaining = length; while (remaining > 0) { const ssize_t written = send(fd, cursor, remaining, 0); if (written <= 0) throw std::runtime_error(std::string(context) + ": send failed: " + std::strerror(errno)); cursor += written; remaining -= static_cast(written); } } void ReadExact(int fd, void* data, size_t length, std::string_view context) { uint8_t* cursor = static_cast(data); size_t remaining = length; while (remaining > 0) { const ssize_t bytes_read = recv(fd, cursor, remaining, 0); if (bytes_read <= 0) throw std::runtime_error(std::string(context) + ": recv failed: " + std::strerror(errno)); cursor += bytes_read; remaining -= static_cast(bytes_read); } } bool WaitForReadable(int fd, int timeout_ms) { pollfd descriptor {}; descriptor.fd = fd; descriptor.events = POLLIN; const int rc = poll(&descriptor, 1, timeout_ms); if (rc < 0) throw std::runtime_error("poll failed: " + std::string(std::strerror(errno))); return rc > 0 && (descriptor.revents & POLLIN); } int ConnectTcp(const std::string& host, uint16_t port) { const int fd = socket(AF_INET, SOCK_STREAM, 0); Expect(fd >= 0, "socket() failed"); timeval timeout {}; timeout.tv_sec = 5; timeout.tv_usec = 0; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); sockaddr_in addr {}; addr.sin_family = AF_INET; addr.sin_port = htons(port); Expect(inet_pton(AF_INET, host.c_str(), &addr.sin_addr) == 1, "inet_pton() failed"); Expect(connect(fd, reinterpret_cast(&addr), sizeof(addr)) == 0, "connect() failed: " + std::string(std::strerror(errno))); return fd; } class EncryptedClient { public: EncryptedClient(const std::string& host, uint16_t port) : m_fd(ConnectTcp(host, port)) { Expect(m_cipher.Initialize(), "SecureCipher init failed"); } ~EncryptedClient() { if (m_fd >= 0) close(m_fd); } void Handshake() { PacketGCPhase phase {}; ReadExact(m_fd, &phase, sizeof(phase), "read initial phase"); Expect(phase.header == GC::PHASE, "unexpected initial phase header"); PacketGCKeyChallenge challenge {}; ReadExact(m_fd, &challenge, sizeof(challenge), "read key challenge"); Expect(challenge.header == GC::KEY_CHALLENGE, "unexpected key challenge header"); Expect(m_cipher.ComputeClientKeys(challenge.server_pk), "client key derivation failed"); PacketCGKeyResponse response {}; response.header = CG::KEY_RESPONSE; response.length = sizeof(response); m_cipher.GetPublicKey(response.client_pk); m_cipher.ComputeChallengeResponse(challenge.challenge, response.challenge_response); WriteExact(m_fd, &response, sizeof(response), "write key response"); PacketGCKeyComplete complete {}; ReadExact(m_fd, &complete, sizeof(complete), "read key complete"); Expect(complete.header == GC::KEY_COMPLETE, "unexpected key complete header"); std::array session_token {}; Expect(m_cipher.DecryptToken(complete.encrypted_token, sizeof(complete.encrypted_token), complete.nonce, session_token.data()), "decrypt token failed"); m_cipher.SetSessionToken(session_token.data()); m_cipher.SetActivated(true); } template void SendEncryptedPacket(const TPacket& packet) { TPacket encrypted = packet; m_cipher.EncryptInPlace(&encrypted, sizeof(encrypted)); WriteExact(m_fd, &encrypted, sizeof(encrypted), "write encrypted packet"); } bool WaitForFrame(std::vector& frame, int timeout_ms) { while (true) { if (TryPopFrame(frame)) return true; if (!WaitForReadable(m_fd, timeout_ms)) return false; ReadEncryptedChunk(); } } private: bool TryPopFrame(std::vector& frame) { if (m_stream.size() < PACKET_HEADER_SIZE) return false; const auto header = *reinterpret_cast(m_stream.data()); const auto length = *reinterpret_cast(m_stream.data() + sizeof(uint16_t)); Expect(length >= PACKET_HEADER_SIZE && length <= 8192, "invalid encrypted packet length header=" + std::to_string(header) + " length=" + std::to_string(length)); if (m_stream.size() < length) return false; frame.assign(m_stream.begin(), m_stream.begin() + length); m_stream.erase(m_stream.begin(), m_stream.begin() + length); return true; } void ReadEncryptedChunk() { std::array buffer {}; const ssize_t bytes_read = recv(m_fd, buffer.data(), buffer.size(), 0); if (bytes_read <= 0) throw std::runtime_error("recv failed: " + std::string(std::strerror(errno))); const size_t old_size = m_stream.size(); m_stream.resize(old_size + static_cast(bytes_read)); std::memcpy(m_stream.data() + old_size, buffer.data(), static_cast(bytes_read)); m_cipher.DecryptInPlace(m_stream.data() + old_size, static_cast(bytes_read)); } int m_fd = -1; SecureCipher m_cipher; std::vector m_stream; }; uint16_t FrameHeader(const std::vector& frame) { return *reinterpret_cast(frame.data()); } uint16_t FrameLength(const std::vector& frame) { return *reinterpret_cast(frame.data() + sizeof(uint16_t)); } int RemainingTimeoutMs(std::chrono::steady_clock::time_point deadline) { const auto now = std::chrono::steady_clock::now(); if (now >= deadline) return 0; return static_cast(std::chrono::duration_cast(deadline - now).count()); } uint8_t FindFirstPlayableCharacterIndex(const PacketGCLoginSuccess4& success) { for (uint8_t index = 0; index < PLAYER_PER_ACCOUNT_LOCAL; ++index) { if (success.players[index].id != 0 && success.players[index].change_name == 0) return index; } throw std::runtime_error("account has no playable characters"); } AuthResponse Authenticate(EncryptedClient& auth_client, const std::string& login, const std::string& password, SmokeResult& result, bool json) { PacketCGLogin3 login_packet {}; login_packet.header = CG::LOGIN3; login_packet.length = sizeof(login_packet); std::strncpy(login_packet.login, login.c_str(), sizeof(login_packet.login) - 1); std::strncpy(login_packet.passwd, password.c_str(), sizeof(login_packet.passwd) - 1); auth_client.SendEncryptedPacket(login_packet); std::vector frame; for (int i = 0; i < 8; ++i) { Expect(auth_client.WaitForFrame(frame, 5000), "timed out waiting for auth response"); const auto header = FrameHeader(frame); if (header == GC::PHASE) continue; if (header == GC::LOGIN_FAILURE) { Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected login failure size"); const auto* failure = reinterpret_cast(frame.data()); EmitEvent(result, json, "auth_failure status=" + std::string(failure->status)); return AuthResponse {false, 0, failure->status}; } if (header == GC::AUTH_SUCCESS) { Expect(frame.size() == sizeof(PacketGCAuthSuccess), "unexpected auth success size"); const auto* success = reinterpret_cast(frame.data()); Expect(success->result != 0, "auth result returned failure"); EmitEvent(result, json, "auth_success login_key=" + std::to_string(success->login_key)); return AuthResponse {true, success->login_key, {}}; } } throw std::runtime_error("did not receive AUTH_SUCCESS"); } ChannelLoginResponse LoginToChannel(EncryptedClient& channel_client, const std::string& login, uint32_t login_key, SmokeResult& result, bool json) { PacketCGLogin2 login_packet {}; login_packet.header = CG::LOGIN2; login_packet.length = sizeof(login_packet); login_packet.login_key = login_key; std::strncpy(login_packet.login, login.c_str(), sizeof(login_packet.login) - 1); channel_client.SendEncryptedPacket(login_packet); bool saw_empire = false; bool saw_login_success = false; uint8_t empire = 0; PacketGCLoginSuccess4 login_success {}; std::vector frame; for (int i = 0; i < 16; ++i) { Expect(channel_client.WaitForFrame(frame, 5000), "timed out waiting for channel response"); const auto header = FrameHeader(frame); const auto length = FrameLength(frame); if (header == GC::PHASE) continue; if (header == GC::LOGIN_FAILURE) { Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected channel login failure size"); const auto* failure = reinterpret_cast(frame.data()); EmitEvent(result, json, "channel_failure status=" + std::string(failure->status)); return ChannelLoginResponse {false, -1, failure->status, {}}; } if (header == GC::EMPIRE) { Expect(frame.size() == sizeof(PacketGCEmpire), "unexpected empire packet size"); const auto* empire_packet = reinterpret_cast(frame.data()); saw_empire = true; empire = empire_packet->empire; EmitEvent(result, json, "channel_empire empire=" + std::to_string(static_cast(empire))); continue; } if (header == GC::LOGIN_SUCCESS4) { Expect(frame.size() == sizeof(PacketGCLoginSuccess4), "unexpected channel login success size"); std::memcpy(&login_success, frame.data(), sizeof(login_success)); saw_login_success = true; EmitEvent(result, json, "channel_login_success length=" + std::to_string(length)); break; } } Expect(saw_empire, "did not receive EMPIRE"); Expect(saw_login_success, "did not receive LOGIN_SUCCESS4"); return ChannelLoginResponse {true, empire, {}, login_success}; } uint8_t CreateCharacter(EncryptedClient& channel_client, std::string_view create_name, SmokeResult& result, bool json) { PacketCGPlayerCreate create_packet {}; create_packet.header = CG::CHARACTER_CREATE; create_packet.length = sizeof(create_packet); create_packet.index = 0; create_packet.job = 0; create_packet.shape = 0; create_packet.con = 4; create_packet.intel = 4; create_packet.str = 4; create_packet.dex = 4; std::strncpy(create_packet.name, create_name.data(), sizeof(create_packet.name) - 1); channel_client.SendEncryptedPacket(create_packet); std::vector frame; for (int i = 0; i < 16; ++i) { Expect(channel_client.WaitForFrame(frame, 5000), "timed out waiting for character create response"); const auto header = FrameHeader(frame); if (header == GC::PHASE) continue; if (header == GC::PLAYER_CREATE_FAILURE) { if (frame.size() == sizeof(PacketGCCreateFailure)) { const auto* failure = reinterpret_cast(frame.data()); throw std::runtime_error("character create failed type=" + std::to_string(failure->type)); } throw std::runtime_error("character create failed"); } if (header == GC::PLAYER_CREATE_SUCCESS) { Expect(frame.size() == sizeof(PacketGCPlayerCreateSuccess), "unexpected player create success size"); const auto* success = reinterpret_cast(frame.data()); EmitEvent(result, json, "character_created index=" + std::to_string(static_cast(success->account_character_index)) + " name=" + success->player.name); result.character_created = true; result.character_name = success->player.name; return success->account_character_index; } } throw std::runtime_error("did not receive PLAYER_CREATE_SUCCESS"); } void SelectCharacter(EncryptedClient& channel_client, uint8_t character_index, SmokeResult& result, bool json) { PacketCGPlayerSelect select_packet {}; select_packet.header = CG::CHARACTER_SELECT; select_packet.length = sizeof(select_packet); select_packet.index = character_index; channel_client.SendEncryptedPacket(select_packet); bool saw_phase_loading = false; bool saw_main_character = false; 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 character select response"); Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for character select response"); const auto header = FrameHeader(frame); if (header == GC::LOGIN_FAILURE) { Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected character select failure size"); const auto* failure = reinterpret_cast(frame.data()); throw std::runtime_error(std::string("character select failed: ") + failure->status); } if (header == GC::PHASE) { Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size"); const auto* phase = reinterpret_cast(frame.data()); if (phase->phase == PHASE_LOADING) { saw_phase_loading = true; EmitEvent(result, json, "phase_loading"); } continue; } if (header == GC::MAIN_CHARACTER) { Expect(frame.size() == sizeof(PacketGCMainCharacter), "unexpected main character packet size"); const auto* main_character = reinterpret_cast(frame.data()); Expect(main_character->name[0] != '\0', "main character name is empty"); saw_main_character = true; result.character_name = main_character->name; EmitEvent(result, json, "main_character name=" + std::string(main_character->name)); } if (saw_phase_loading && saw_main_character) return; } } void SendClientVersion(EncryptedClient& channel_client, std::string_view client_version, SmokeResult& result, bool json) { PacketCGClientVersion version_packet {}; version_packet.header = CG::CLIENT_VERSION; version_packet.length = sizeof(version_packet); std::strncpy(version_packet.filename, "Metin2_Debug.exe", sizeof(version_packet.filename) - 1); std::strncpy(version_packet.timestamp, client_version.data(), sizeof(version_packet.timestamp) - 1); channel_client.SendEncryptedPacket(version_packet); EmitEvent(result, json, "client_version " + std::string(version_packet.timestamp)); } void EnterGame(EncryptedClient& channel_client, SmokeResult& result, bool json) { PacketCGEnterGame enter_game {}; enter_game.header = CG::ENTERGAME; enter_game.length = sizeof(enter_game); channel_client.SendEncryptedPacket(enter_game); bool saw_phase_game = false; bool saw_time = false; bool saw_channel = false; 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 entergame response"); Expect(channel_client.WaitForFrame(frame, timeout_ms), "timed out waiting for entergame response"); const auto header = FrameHeader(frame); if (header == GC::LOGIN_FAILURE) { Expect(frame.size() == sizeof(PacketGCLoginFailure), "unexpected entergame failure size"); const auto* failure = reinterpret_cast(frame.data()); throw std::runtime_error(std::string("entergame failed: ") + failure->status); } if (header == GC::PHASE) { Expect(frame.size() == sizeof(PacketGCPhase), "unexpected phase packet size"); const auto* phase = reinterpret_cast(frame.data()); if (phase->phase == PHASE_GAME) { saw_phase_game = true; EmitEvent(result, json, "phase_game"); } continue; } if (header == GC::TIME) { Expect(frame.size() == sizeof(PacketGCTime), "unexpected time packet size"); saw_time = true; continue; } if (header == GC::CHANNEL) { Expect(frame.size() == sizeof(PacketGCChannel), "unexpected channel packet size"); const auto* channel = reinterpret_cast(frame.data()); saw_channel = true; result.game_channel = channel->channel; EmitEvent(result, json, "game_channel channel=" + std::to_string(static_cast(channel->channel))); } if (saw_phase_game && saw_time && saw_channel) return; } } } int main(int argc, char** argv) { SmokeOptions options; SmokeResult result; try { Expect(argc >= 6, "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]"); const std::string host = argv[1]; const uint16_t auth_port = static_cast(std::stoi(argv[2])); const uint16_t channel_port = static_cast(std::stoi(argv[3])); const std::string login = argv[4]; std::string password; const std::string password_arg = argv[5]; const std::string prefix = "--password-env="; if (password_arg.rfind(prefix, 0) == 0) { const char* value = std::getenv(password_arg.substr(prefix.size()).c_str()); Expect(value && *value, "password environment variable is empty"); password = value; } else { password = password_arg; } for (int i = 6; i < argc; ++i) { const std::string create_arg = argv[i]; const std::string create_prefix = "--create-character-name="; 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="; if (create_arg.rfind(create_prefix, 0) == 0) { options.create_character_name = create_arg.substr(create_prefix.size()); Expect(!options.create_character_name.empty(), "create character name is empty"); Expect(options.create_character_name.size() <= CHARACTER_NAME_MAX_LEN_LOCAL, "create character name too long"); continue; } if (create_arg.rfind(version_prefix, 0) == 0) { options.client_version = create_arg.substr(version_prefix.size()); Expect(!options.client_version.empty(), "client version is empty"); continue; } if (create_arg.rfind(auth_failure_prefix, 0) == 0) { options.expect_auth_failure = create_arg.substr(auth_failure_prefix.size()); Expect(!options.expect_auth_failure.empty(), "expected auth failure status is empty"); continue; } if (create_arg.rfind(channel_failure_prefix, 0) == 0) { options.expect_channel_failure = create_arg.substr(channel_failure_prefix.size()); Expect(!options.expect_channel_failure.empty(), "expected channel failure status is empty"); continue; } if (create_arg == "--json") { options.json = true; continue; } throw std::runtime_error("unknown optional argument: " + create_arg); } Expect(login.size() <= LOGIN_MAX_LEN_LOCAL, "login too long"); Expect(password.size() <= PASSWD_MAX_LEN_LOCAL, "password too long"); result.stage = "auth_handshake"; auto started_at = std::chrono::steady_clock::now(); EncryptedClient auth_client(host, auth_port); auth_client.Handshake(); result.auth_handshake_ms = ElapsedMs(started_at); result.stage = "auth_login"; started_at = std::chrono::steady_clock::now(); const AuthResponse auth = Authenticate(auth_client, login, password, result, options.json); result.auth_login_ms = ElapsedMs(started_at); result.failure_status.clear(); if (!auth.success) { result.failure_status = auth.failure_status; if (!options.expect_auth_failure.empty()) { Expect(auth.failure_status == options.expect_auth_failure, "expected auth failure '" + options.expect_auth_failure + "' but got '" + auth.failure_status + "'"); result.ok = true; result.result = "expected_auth_failure"; result.stage = "auth_login"; EmitEvent(result, options.json, "expected_auth_failure status=" + auth.failure_status); if (options.json) PrintJson(result); return 0; } throw std::runtime_error("auth login failed: " + auth.failure_status); } Expect(options.expect_auth_failure.empty(), "auth login succeeded but expected failure '" + options.expect_auth_failure + "'"); const uint32_t login_key = auth.login_key; result.login_key = login_key; result.stage = "channel_handshake"; started_at = std::chrono::steady_clock::now(); EncryptedClient channel_client(host, channel_port); channel_client.Handshake(); result.channel_handshake_ms = ElapsedMs(started_at); result.stage = "channel_login"; started_at = std::chrono::steady_clock::now(); const ChannelLoginResponse channel = LoginToChannel(channel_client, login, login_key, result, options.json); result.channel_login_ms = ElapsedMs(started_at); if (!channel.success) { result.failure_status = channel.failure_status; if (!options.expect_channel_failure.empty()) { Expect(channel.failure_status == options.expect_channel_failure, "expected channel failure '" + options.expect_channel_failure + "' but got '" + channel.failure_status + "'"); result.ok = true; result.result = "expected_channel_failure"; result.stage = "channel_login"; EmitEvent(result, options.json, "expected_channel_failure status=" + channel.failure_status); if (options.json) PrintJson(result); return 0; } throw std::runtime_error("channel login failed: " + channel.failure_status); } Expect(options.expect_channel_failure.empty(), "channel login succeeded but expected failure '" + options.expect_channel_failure + "'"); const PacketGCLoginSuccess4& login_success = channel.login_success; result.empire = channel.empire; uint8_t character_index = 0; try { character_index = FindFirstPlayableCharacterIndex(login_success); result.character_index = character_index; result.character_name = login_success.players[character_index].name; EmitEvent(result, options.json, "selected_character index=" + std::to_string(static_cast(character_index))); } catch (const std::exception&) { Expect(!options.create_character_name.empty(), "account has no playable characters and no create-character name was provided"); result.stage = "character_create"; started_at = std::chrono::steady_clock::now(); character_index = CreateCharacter(channel_client, options.create_character_name, result, options.json); result.character_create_ms = ElapsedMs(started_at); result.character_index = character_index; } result.stage = "character_select"; started_at = std::chrono::steady_clock::now(); SelectCharacter(channel_client, character_index, result, options.json); result.character_select_ms = ElapsedMs(started_at); result.stage = "client_version"; SendClientVersion(channel_client, options.client_version, result, options.json); result.stage = "entergame"; started_at = std::chrono::steady_clock::now(); EnterGame(channel_client, result, options.json); result.entergame_ms = ElapsedMs(started_at); result.ok = true; result.result = "success"; result.stage = "complete"; EmitEvent(result, options.json, "full_login_success"); if (options.json) PrintJson(result); return 0; } catch (const std::exception& e) { result.ok = false; result.result = "unexpected_failure"; result.error = e.what(); if (options.json) { PrintJson(result); } else { std::cerr << "metin_login_smoke failed: " << e.what() << "\n"; } return 1; } }