/* EQ2Emulator: Everquest II Server Emulator Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) This file is part of EQ2Emulator. */ // Debug includes #include "../common/debug.h" // Linux network headers #include #include #include #include // Standard library includes #include #include #include #include #include #include #include #include #include // Project includes #include "net.h" #include "client.h" #include "../common/EQStream.h" #include "../common/packet_dump.h" #include "../common/packet_functions.h" #include "../common/emu_opcodes.h" #include "../common/MiscFunctions.h" #include "LWorld.h" #include "LoginDatabase.h" #include "../common/ConfigReader.h" #include "../common/Log.h" // Global instances extern NetConnection net; extern LWorldList world_list; extern ClientList client_list; extern LoginDatabase database; extern std::map EQOpcodeManager; extern ConfigReader configReader; using namespace std; /** * @brief Constructs a new Client instance * @param ieqnc The EQStream connection for this client */ Client::Client(EQStream* ieqnc) : eqnc_{ieqnc} , ip_{ieqnc->GetrIP()} , port_{ntohs(ieqnc->GetrPort())} , account_id_{0} , lsadmin_{0} , worldadmin_{0} , lsstatus_{0} , version_{0} , kicked_{false} , verified_{false} , login_mode_{LoginMode::None} , num_updates_{0} , request_num_{0} , createRequest_{nullptr} , playWaitTimer_{nullptr} , start_{false} , update_position_{0} , update_packets_{nullptr} , needs_world_list_{true} , sent_character_list_{false} { // Clear critical buffers std::memset(bannedreason_, 0, sizeof(bannedreason_)); std::memset(key_, 0, sizeof(key_)); std::memset(ClientSession, 0, sizeof(ClientSession)); // Initialize timers updatetimer = std::make_unique(500); updatelisttimer = std::make_unique(10000); } /** * @brief Destructor - cleans up all client resources */ Client::~Client() { // Close the connection if (eqnc_) { eqnc_->Close(); } // Clean up character creation request safe_delete(createRequest_); // Clean up update packets if (update_packets_) { for (auto* packet : *update_packets_) { safe_delete(packet); } safe_delete(update_packets_); } } /** * @brief Main processing function for the client * @return true if client is still active, false if should be disconnected */ bool Client::Process() { // Handle initial connection state if (!start_ && !eqnc_->CheckActive()) { if (!playWaitTimer_) { playWaitTimer_ = std::make_unique(5000); } else if (playWaitTimer_->Check()) { playWaitTimer_.reset(); return false; } return true; } else if (!start_) { playWaitTimer_.reset(); start_ = true; } // Check disconnect timer if (disconnectTimer && disconnectTimer->Check()) { disconnectTimer.reset(); getConnection()->SendDisconnect(); } // Process packets if not kicked if (!kicked_) { // Handle play wait timeout if (playWaitTimer_ != nullptr && playWaitTimer_->Check()) { SendPlayFailed(PLAY_ERROR_SERVER_TIMEOUT); playWaitTimer_.reset(); } // Process world list updates if (!needs_world_list_ && updatetimer && updatetimer->Check()) { if (updatelisttimer && updatelisttimer->Check()) { // Disconnect after 30 minutes of updates if (num_updates_ >= 180) { getConnection()->SendDisconnect(); } else { // Clean up old update packets if (update_packets_) { for (auto* packet : *update_packets_) { safe_delete(packet); } } safe_delete(update_packets_); // Get new update packets update_packets_ = world_list.GetServerListUpdate(version_); } num_updates_++; } else { // Send update packets in sequence if (!update_packets_) { update_packets_ = world_list.GetServerListUpdate(version_); } else { if (update_position_ < update_packets_->size()) { QueuePacket(update_packets_->at(update_position_)->serialize()); update_position_++; } else { update_position_ = 0; } } } } // Process incoming packets EQApplicationPacket* app = nullptr; while ((app = eqnc_->PopPacket())) { switch (app->GetOpcode()) { case OP_LoginRequestMsg: { ProcessLoginRequest(app); break; } case OP_KeymapLoadMsg: { // Keymap message - currently unused break; } case OP_AllWSDescRequestMsg: { ProcessWorldListRequest(); break; } case OP_LsClientCrashlogReplyMsg: { SaveErrorsToDB(app, "Crash Log", GetVersion()); break; } case OP_LsClientVerifylogReplyMsg: { SaveErrorsToDB(app, "Verify Log", GetVersion()); break; } case OP_LsClientAlertlogReplyMsg: { SaveErrorsToDB(app, "Alert Log", GetVersion()); break; } case OP_LsClientBaselogReplyMsg: { SaveErrorsToDB(app, "Base Log", GetVersion()); break; } case OP_AllCharactersDescRequestMsg: { // Character description request - handled elsewhere break; } case OP_CreateCharacterRequestMsg: { ProcessCharacterCreation(app); break; } case OP_PlayCharacterRequestMsg: { ProcessPlayCharacter(app); break; } case OP_DeleteCharacterRequestMsg: { ProcessDeleteCharacter(app); break; } default: { const char* name = app->GetOpcodeName(); if (name) { LogWrite(OPCODE__DEBUG, 1, "Opcode", "%s Received %04X (%i)", name, app->GetRawOpcode(), app->GetRawOpcode()); } else { LogWrite(OPCODE__DEBUG, 1, "Opcode", "Received %04X (%i)", app->GetRawOpcode(), app->GetRawOpcode()); } break; } } delete app; } } // Check if connection is still active if (!eqnc_->CheckActive()) { return false; } return true; } /** * @brief Processes a login request packet * @param app The login request packet */ void Client::ProcessLoginRequest(EQApplicationPacket* app) { DumpPacket(app); // Try loading with version 1 structure auto packet = std::unique_ptr(configReader.getStruct("LS_LoginRequest", 1)); if (packet && packet->LoadPacketData(app->pBuffer, app->size)) { version_ = packet->getType_int16_ByName("version"); LogWrite(LOGIN__DEBUG, 0, "Login", "Classic Client Version Provided: %i", version_); // Check if we need to try the newer structure if (version_ == 0 || EQOpcodeManager.count(GetOpcodeVersion(version_)) == 0) { packet.reset(configReader.getStruct("LS_LoginRequest", 1208)); if (packet && packet->LoadPacketData(app->pBuffer, app->size)) { version_ = packet->getType_int16_ByName("version"); } else { return; } } LogWrite(LOGIN__DEBUG, 0, "Login", "New Client Version Provided: %i", version_); // Verify client version is supported if (EQOpcodeManager.count(GetOpcodeVersion(version_)) == 0) { LogWrite(LOGIN__ERROR, 0, "Login", "Incompatible client version provided: %i", version_); SendLoginDenied(); return; } // Process login credentials if (EQOpcodeManager.count(GetOpcodeVersion(version_)) > 0 && getConnection()) { getConnection()->SetClientVersion(GetVersion()); EQ2_16BitString username = packet->getType_EQ2_16BitString_ByName("username"); EQ2_16BitString password = packet->getType_EQ2_16BitString_ByName("password"); // Load account from database LoginAccount* acct = database.LoadAccount(username.data.c_str(), password.data.c_str(), net.IsAllowingAccountCreation()); // Check for duplicate login if (acct) { Client* otherclient = client_list.FindByLSID(acct->getLoginAccountID()); if (otherclient) { // Kick the previous client (might be a ghost) otherclient->getConnection()->SendDisconnect(); } } // Process successful login if (acct) { SetAccountName(username.data.c_str()); database.UpdateAccountIPAddress(acct->getLoginAccountID(), getConnection()->GetrIP()); database.UpdateAccountClientDataVersion(acct->getLoginAccountID(), version_); LogWrite(LOGIN__INFO, 0, "Login", "%s successfully logged in.", username.data.c_str()); needs_world_list_ = true; SetLoginAccount(acct); SendLoginAccepted(); } else { // Login failed if (username.size > 0) { LogWrite(LOGIN__ERROR, 0, "Login", "%s login failed!", username.data.c_str()); } else { LogWrite(LOGIN__ERROR, 0, "Login", "[UNKNOWN USER] login failed!"); } SendLoginDenied(); } } else { std::cout << "Error bad version: " << version_ << std::endl; SendLoginDeniedBadVersion(); } } else { std::cout << "Error loading LS_LoginRequest packet" << std::endl; } } /** * @brief Processes a world list request */ void Client::ProcessWorldListRequest() { SendWorldList(); needs_world_list_ = false; if (!sent_character_list_) { database.LoadCharacters(GetLoginAccount(), GetVersion()); sent_character_list_ = true; } SendCharList(); } /** * @brief Processes a character creation request * @param app The character creation packet */ void Client::ProcessCharacterCreation(EQApplicationPacket* app) { auto packet = std::unique_ptr( configReader.getStruct("CreateCharacter", GetVersion())); DumpPacket(app); // Start play wait timer playWaitTimer_ = std::make_unique(15000); playWaitTimer_->Start(); LogWrite(WORLD__INFO, 1, "World", "Character creation request from account %s", GetAccountName()); if (packet->LoadPacketData(app->pBuffer, app->size, GetVersion() <= 561 ? false : true)) { DumpPacket(app->pBuffer, app->size); packet->setDataByName("account_id", GetAccountID()); // Find the target world server LWorld* world_server = world_list.FindByID(packet->getType_int32_ByName("server_id")); if (!world_server) { DumpPacket(app->pBuffer, app->size); std::cout << GetAccountName() << " attempted creation of character with an invalid server id of: " << packet->getType_int32_ByName("server_id") << "\n"; return; } // Store creation request and forward to world server createRequest_ = packet.release(); auto outpack = std::make_unique(ServerOP_CharacterCreate, app->size + sizeof(std::int16_t)); std::int16_t out_version = GetVersion(); std::memcpy(outpack->pBuffer, &out_version, sizeof(std::int16_t)); std::memcpy(outpack->pBuffer + sizeof(std::int16_t), app->pBuffer, app->size); // Adjust pointer based on version unsigned char* tmp = outpack->pBuffer; if (out_version <= 283) { tmp += 2; } else if (out_version == 373) { tmp += 6; } else { tmp += 7; } // Add account ID std::int32_t account_id = GetAccountID(); std::memcpy(tmp, &account_id, sizeof(std::int32_t)); world_server->SendPacket(outpack.get()); } else { LogWrite(WORLD__ERROR, 1, "World", "Error in character creation request from account %s!", GetAccountName()); } } /** * @brief Processes a play character request * @param app The play character packet */ void Client::ProcessPlayCharacter(EQApplicationPacket* app) { std::int32_t char_id = 0; std::int32_t server_id = 0; auto request = std::unique_ptr( configReader.getStruct("LS_PlayRequest", GetVersion())); if (request && request->LoadPacketData(app->pBuffer, app->size)) { char_id = request->getType_int32_ByName("char_id"); // Handle version-specific server ID retrieval if (GetVersion() <= 283) { server_id = database.GetServer(GetAccountID(), char_id, request->getType_EQ2_16BitString_ByName("name").data); } else { server_id = request->getType_int32_ByName("server_id"); } // Find world server and character LWorld* world = world_list.FindByID(server_id); std::string name = database.GetCharacterName(char_id, server_id, GetAccountID()); if (world && !name.empty()) { pending_play_char_id_ = char_id; // Create user to world request auto outpack = std::make_unique(ServerOP_UsertoWorldReq, sizeof(UsertoWorldRequest_Struct)); auto* req = reinterpret_cast(outpack->pBuffer); req->char_id = char_id; req->lsaccountid = GetAccountID(); req->worldid = server_id; // Set IP address struct in_addr in; in.s_addr = GetIP(); std::strcpy(req->ip_address, inet_ntoa(in)); world->SendPacket(outpack.get()); // Start play wait timer playWaitTimer_.reset(); playWaitTimer_ = std::make_unique(5000); playWaitTimer_->Start(); } else { std::cout << GetAccountName() << " sent invalid Play Request" << std::endl; SendPlayFailed(PLAY_ERROR_PROBLEM); DumpPacket(app); } } } /** * @brief Processes a delete character request * @param app The delete character packet */ void Client::ProcessDeleteCharacter(EQApplicationPacket* app) { auto request = std::unique_ptr( configReader.getStruct("LS_DeleteCharacterRequest", GetVersion())); auto response = std::unique_ptr( configReader.getStruct("LS_DeleteCharacterResponse", GetVersion())); if (request && response && request->LoadPacketData(app->pBuffer, app->size)) { EQ2_16BitString name = request->getType_EQ2_16BitString_ByName("name"); std::int32_t acct_id = GetAccountID(); std::int32_t char_id = request->getType_int32_ByName("char_id"); std::int32_t server_id = request->getType_int32_ByName("server_id"); // Verify deletion if (database.VerifyDelete(acct_id, char_id, name.data.c_str())) { response->setDataByName("response", 1); GetLoginAccount()->removeCharacter(name.data.c_str(), GetVersion()); // Notify world server LWorld* world_server = world_list.FindByID(server_id); if (world_server != nullptr) { world_server->SendDeleteCharacter(char_id, acct_id); } } else { response->setDataByName("response", 0); } // Build response response->setDataByName("server_id", server_id); response->setDataByName("char_id", char_id); response->setDataByName("account_id", account_id_); response->setMediumStringByName("name", name.data.c_str()); response->setDataByName("max_characters", 10); EQ2Packet* outapp = response->serialize(); QueuePacket(outapp); // Refresh character list SendCharList(); } } /** * @brief Saves client error logs to the database * @param app Packet containing error data * @param type Type of error log * @param version Client version */ void Client::SaveErrorsToDB(EQApplicationPacket* app, const char* type, std::int32_t version) { std::int32_t size = 0; z_stream zstream{}; // Extract size and data based on version if (version >= 546) { std::memcpy(&size, app->pBuffer + sizeof(std::int32_t), sizeof(std::int32_t)); zstream.next_in = app->pBuffer + 8; zstream.avail_in = app->size - 8; } else { // Box set version size = 0xFFFF; zstream.next_in = app->pBuffer + 2; zstream.avail_in = app->size - 2; } size++; auto message = std::make_unique(size); std::memset(message.get(), 0, size); // Setup decompression zstream.next_out = reinterpret_cast(message.get()); zstream.avail_out = size; zstream.zalloc = Z_NULL; zstream.zfree = Z_NULL; zstream.opaque = Z_NULL; // Decompress message int zerror = inflateInit(&zstream); if (zerror != Z_OK) { return; } zerror = inflate(&zstream, 0); inflateEnd(&zstream); // Save to database if message is valid if (message && std::strlen(message.get()) > 0) { database.SaveClientLog(type, message.get(), GetLoginAccount()->getLoginName(), GetVersion()); } } /** * @brief Handles character creation approval from world server * @param server_id World server ID * @param char_id New character ID */ void Client::CharacterApproved(std::int32_t server_id, std::int32_t char_id) { if (createRequest_ && server_id == createRequest_->getType_int32_ByName("server_id")) { LWorld* world_server = world_list.FindByID(server_id); if (!world_server) { return; } // Send success response auto packet = std::unique_ptr( configReader.getStruct("LS_CreateCharacterReply", GetVersion())); if (packet) { packet->setDataByName("account_id", GetAccountID()); packet->setDataByName("unknown", 0xFFFFFFFF); packet->setDataByName("response", CREATESUCCESS_REPLY); packet->setMediumStringByName("name", createRequest_->getType_EQ2_16BitString_ByName("name").data.c_str()); EQ2Packet* outapp = packet->serialize(); QueuePacket(outapp); // Save character to database database.SaveCharacter(createRequest_, GetLoginAccount(), char_id, GetVersion()); // Refresh character list database.LoadCharacters(GetLoginAccount(), GetVersion()); SendCharList(); // Auto-enter world for older clients if (GetVersion() <= 561) { pending_play_char_id_ = char_id; auto outpack = std::make_unique(ServerOP_UsertoWorldReq, sizeof(UsertoWorldRequest_Struct)); auto* req = reinterpret_cast(outpack->pBuffer); req->char_id = char_id; req->lsaccountid = GetAccountID(); req->worldid = server_id; struct in_addr in; in.s_addr = GetIP(); std::strcpy(req->ip_address, inet_ntoa(in)); world_server->SendPacket(outpack.get()); } } } else { std::cout << GetAccountName() << " received invalid CharacterApproval from server: " << server_id << std::endl; } safe_delete(createRequest_); } /** * @brief Handles character creation rejection * @param reason_number Rejection reason code */ void Client::CharacterRejected(std::int8_t reason_number) { auto packet = std::unique_ptr( configReader.getStruct("LS_CreateCharacterReply", GetVersion())); if (createRequest_ && packet) { packet->setDataByName("account_id", GetAccountID()); packet->setDataByName("response", reason_number); packet->setMediumStringByName("name", ""); EQ2Packet* outapp = packet->serialize(); QueuePacket(outapp); } safe_delete(createRequest_); } /** * @brief Sends the character list to the client */ void Client::SendCharList() { LogWrite(LOGIN__INFO, 0, "Login", "[%s] sending character list.", GetAccountName()); LS_CharSelectList list; list.loadData(GetAccountID(), GetLoginAccount()->charlist, GetVersion()); EQ2Packet* outapp = list.serialize(GetVersion()); DumpPacket(outapp->pBuffer, outapp->size); QueuePacket(outapp); } /** * @brief Sends login denied due to bad version */ void Client::SendLoginDeniedBadVersion() { auto app = std::make_unique(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); auto* ls_response = reinterpret_cast(app->pBuffer); ls_response->reply_code = 6; // Version mismatch ls_response->unknown03 = 0xFFFFFFFF; ls_response->unknown04 = 0xFFFFFFFF; QueuePacket(app.release()); StartDisconnectTimer(); } /** * @brief Sends login denied message */ void Client::SendLoginDenied() { auto app = std::make_unique(OP_LoginReplyMsg, 0, sizeof(LS_LoginResponse)); auto* ls_response = reinterpret_cast(app->pBuffer); // Reply codes for AoM: // 1 = Invalid username or password // 2 = Account already playing // 6 = Version mismatch // 7 = No scheduled playtimes // 8 = Missing features // 11 = Build mismatch // 12 = Must update password ls_response->reply_code = 1; ls_response->unknown03 = 0xFFFFFFFF; ls_response->unknown04 = 0xFFFFFFFF; QueuePacket(app.release()); StartDisconnectTimer(); } /** * @brief Sends login accepted message * @param account_id Account ID * @param login_response Login response code */ void Client::SendLoginAccepted(std::int32_t account_id, std::int8_t login_response) { auto packet = std::unique_ptr( configReader.getStruct("LS_LoginReplyMsg", GetVersion())); if (packet) { packet->setDataByName("account_id", account_id); packet->setDataByName("login_response", login_response); packet->setDataByName("do_not_force_soga", 1); // Set subscription and expansion flags packet->setDataByName("sub_level", net.GetDefaultSubscriptionLevel()); packet->setDataByName("race_flag", 0x1FFFFF); packet->setDataByName("class_flag", 0x7FFFFFE); packet->setMediumStringByName("username", GetAccountName()); packet->setMediumStringByName("password", GetAccountName()); // Expansion flags packet->setDataByName("unknown5", net.GetExpansionFlag()); packet->setDataByName("unknown6", 0xFF); packet->setDataByName("unknown6", 0xFF, 1); packet->setDataByName("unknown6", 0xFF, 2); // Class access packet->setDataByName("unknown10", 0xFF); // Race and city flags packet->setDataByName("unknown7", net.GetEnabledRaces()); packet->setDataByName("unknown7a", 0xEE); packet->setDataByName("unknown8", net.GetCitiesFlag(), 1); EQ2Packet* outapp = packet->serialize(); QueuePacket(outapp); } } /** * @brief Sends the world server list */ void Client::SendWorldList() { EQ2Packet* pack = world_list.MakeServerListPacket(lsadmin_, version_); EQ2Packet* dupe = pack->Copy(); DumpPacket(dupe->pBuffer, dupe->size); QueuePacket(dupe); // Trigger special client code path SendLoginAccepted(0, 10); } /** * @brief Queues a packet for sending * @param app Packet to queue */ void Client::QueuePacket(EQ2Packet* app) { eqnc_->EQ2QueuePacket(app); } /** * @brief Handles world server response for play request * @param worldid World server ID * @param response Response code * @param ip_address World server IP * @param port World server port * @param access_key Access key */ void Client::WorldResponse(std::int32_t worldid, std::int8_t response, char* ip_address, std::int32_t port, std::int32_t access_key) { LWorld* world = world_list.FindByID(worldid); if (world == nullptr) { FatalError(0); return; } if (response != 1) { // Handle character not loaded error if (response == PLAY_ERROR_CHAR_NOT_LOADED) { std::string pending_play_char_name = database.GetCharacterName( pending_play_char_id_, worldid, GetAccountID()); if (database.VerifyDelete(GetAccountID(), pending_play_char_id_, pending_play_char_name.c_str())) { GetLoginAccount()->removeCharacter(pending_play_char_name.c_str(), GetVersion()); } } FatalError(response); return; } // Send successful play response auto response_packet = std::unique_ptr( configReader.getStruct("LS_PlayResponse", GetVersion())); if (response_packet) { playWaitTimer_.reset(); response_packet->setDataByName("response", 1); response_packet->setSmallStringByName("server", ip_address); response_packet->setDataByName("port", port); response_packet->setDataByName("account_id", GetAccountID()); response_packet->setDataByName("access_code", access_key); EQ2Packet* outapp = response_packet->serialize(); QueuePacket(outapp); } } /** * @brief Sends a fatal error to the client * @param response Error response code */ void Client::FatalError(std::int8_t response) { playWaitTimer_.reset(); SendPlayFailed(response); } /** * @brief Sends play failed message * @param response Failure reason code */ void Client::SendPlayFailed(std::int8_t response) { auto response_packet = std::unique_ptr( configReader.getStruct("LS_PlayResponse", GetVersion())); if (response_packet) { response_packet->setDataByName("response", response); response_packet->setSmallStringByName("server", ""); response_packet->setDataByName("port", 0); response_packet->setDataByName("account_id", GetAccountID()); response_packet->setDataByName("access_code", 0); EQ2Packet* outapp = response_packet->serialize(); QueuePacket(outapp); } } /** * @brief Starts the disconnect timer */ void Client::StartDisconnectTimer() { if (!disconnectTimer) { disconnectTimer = std::make_unique(1000); disconnectTimer->Start(); } } // ClientList implementation /** * @brief Adds a client to the list * @param client Client to add */ void ClientList::Add(Client* client) { std::lock_guard lock(mutex_); client_list_[client] = true; } /** * @brief Finds a client by IP and port * @param ip Client IP * @param port Client port * @return Client pointer or nullptr */ Client* ClientList::Get(std::int32_t ip, std::int16_t port) { std::lock_guard lock(mutex_); for (auto& [client, active] : client_list_) { if (client->GetIP() == ip && client->GetPort() == port) { return client; } } return nullptr; } /** * @brief Finds clients waiting for character creation */ void ClientList::FindByCreateRequest() { Client* found_client = nullptr; { std::lock_guard lock(mutex_); for (auto& [client, active] : client_list_) { if (client->AwaitingCharCreationRequest()) { if (!found_client) { found_client = client; } else { // More than one waiting, don't send rejection found_client = nullptr; break; } } } } if (found_client) { found_client->CharacterRejected(UNKNOWNERROR_REPLY); } } /** * @brief Finds a client by login server account ID * @param lsaccountid Account ID * @return Client pointer or nullptr */ Client* ClientList::FindByLSID(std::int32_t lsaccountid) { std::lock_guard lock(mutex_); for (auto& [client, active] : client_list_) { if (client->GetAccountID() == lsaccountid) { return client; } } return nullptr; } /** * @brief Sends a packet to all connected clients * @param app Packet to send */ void ClientList::SendPacketToAllClients(EQ2Packet* app) { { std::lock_guard lock(mutex_); if (!client_list_.empty()) { for (auto& [client, active] : client_list_) { client->QueuePacket(app->Copy()); } } } safe_delete(app); } /** * @brief Processes all clients in the list */ void ClientList::Process() { std::vector erase_list; // Process clients and identify disconnected ones { std::lock_guard lock(mutex_); for (auto& [client, active] : client_list_) { if (!client->Process()) { erase_list.push_back(client); } } } // Remove disconnected clients if (!erase_list.empty()) { std::lock_guard lock(mutex_); for (Client* client : erase_list) { struct in_addr in; in.s_addr = client->getConnection()->GetRemoteIP(); net.numclients--; LogWrite(LOGIN__INFO, 0, "Login", "Removing client from ip: %s on port %i, Account Name: %s", inet_ntoa(in), ntohs(client->getConnection()->GetRemotePort()), client->GetAccountName()); client->getConnection()->Close(); net.UpdateWindowTitle(); client_list_.erase(client); delete client; } } }