/*! \brief Called when the server accepts the connection. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 2 3 * --------------------------------------------------------- * Size | 1 | 1 | 1 | | * Data | player_id| hostid | authorised |playernames* | * --------------------------------------------------------- */ void ClientLobbyRoomProtocol::connectionAccepted(Event* event) { // At least 3 bytes should remain now if(!checkDataSize(event, 3)) return; NetworkString &data = event->data(); STKPeer* peer = event->getPeer(); // Accepted // ======== Log::info("ClientLobbyRoomProtocol", "The server accepted the connection."); // self profile irr::core::stringw name; if (PlayerManager::getCurrentOnlineState() == PlayerProfile::OS_SIGNED_IN) name = PlayerManager::getCurrentOnlineUserName(); else name = PlayerManager::getCurrentPlayer()->getName(); uint8_t my_player_id = data.getUInt8(); uint8_t my_host_id = data.getUInt8(); uint8_t authorised = data.getUInt8(); // Store this client's authorisation status in the peer information // for the server. event->getPeer()->setAuthorised(authorised!=0); STKHost::get()->setMyHostId(my_host_id); NetworkPlayerProfile* profile = new NetworkPlayerProfile(name, my_player_id, my_host_id); STKHost::get()->getGameSetup()->setLocalMaster(my_player_id); m_setup->setNumLocalPlayers(1); // connection token uint32_t token = data.getToken(); peer->setClientServerToken(token); // Add all players // =============== while (data.size() > 0) { uint8_t player_id = data.getUInt8(); uint8_t host_id = data.getUInt8(); irr::core::stringw name; int bytes_read = data.decodeStringW(&name); NetworkPlayerProfile* profile2 = new NetworkPlayerProfile(name, player_id, host_id); m_setup->addPlayer(profile2); // Inform the network lobby of all players so that the GUI can // show all currently connected players. NetworkingLobby::getInstance()->addPlayer(profile2); } // Add self after other players so that player order is identical // on server and all clients. m_setup->addPlayer(profile); NetworkingLobby::getInstance()->addPlayer(profile); m_server = event->getPeer(); m_state = CONNECTED; } // connectionAccepted
/*! \brief Called when a player asks to select a kart. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 2 * ---------------------------------------------- * Size | 1 | 1 | N | * Data |player id | N (kart name size) | kart name | * ---------------------------------------------- */ void ServerLobbyRoomProtocol::kartSelectionRequested(Event* event) { if(m_state!=SELECTING) { Log::warn("Server", "Received kart selection while in state %d.", m_state); return; } if (!checkDataSize(event, 1)) return; const NetworkString &data = event->data(); STKPeer* peer = event->getPeer(); uint8_t player_id = data.getUInt8(); std::string kart_name; data.decodeString(&kart_name); // check if selection is possible if (!m_selection_enabled) { NetworkString *answer = getNetworkString(2); // selection still not started answer->addUInt8(LE_KART_SELECTION_REFUSED).addUInt8(2); peer->sendPacket(answer); delete answer; return; } // check if somebody picked that kart if (!m_setup->isKartAvailable(kart_name)) { NetworkString *answer = getNetworkString(2); // kart is already taken answer->addUInt8(LE_KART_SELECTION_REFUSED).addUInt8(0); peer->sendPacket(answer); delete answer; return; } // check if this kart is authorized if (!m_setup->isKartAllowed(kart_name)) { NetworkString *answer = getNetworkString(2); // kart is not authorized answer->addUInt8(LE_KART_SELECTION_REFUSED).addUInt8(1); peer->sendPacket(answer); delete answer; return; } // send a kart update to everyone NetworkString *answer = getNetworkString(3+kart_name.size()); // This message must be handled synchronously on the client. answer->setSynchronous(true); // kart update (3), 1, race id answer->addUInt8(LE_KART_SELECTION_UPDATE).addUInt8(player_id) .encodeString(kart_name); sendMessageToPeersChangingToken(answer); delete answer; m_setup->setPlayerKart(player_id, kart_name); } // kartSelectionRequested
/*! \brief Called when the server accepts the connection. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 2 3 7 8 12 * ---------------------------------------------------------- * Size | 1 | 1 | 1 | 4 | 1 | 4 | * Data | 1 | 0 <= race id < 16 | 4 | priv token | 4 | global id | * ---------------------------------------------------------- */ void ClientLobbyRoomProtocol::connectionAccepted(Event* event) { NetworkString &data = event->data(); if (data.size() < 12 || data[0] != 1 || data[2] != 4 || data[7] != 4) // 12 bytes remains now { Log::error("ClientLobbyRoomProtocol", "A message notifying an accepted connection wasn't formated as expected."); return; } STKPeer* peer = event->getPeer(); uint32_t global_id = data.gui32(8); if (global_id == PlayerManager::getCurrentOnlineId()) { Log::info("ClientLobbyRoomProtocol", "The server accepted the connection."); // self profile NetworkPlayerProfile* profile = new NetworkPlayerProfile(); profile->kart_name = ""; profile->race_id = data.gui8(1); profile->user_profile = PlayerManager::getCurrentOnlineProfile(); m_setup->addPlayer(profile); // connection token uint32_t token = data.gui32(3); peer->setClientServerToken(token); // add all players data.removeFront(12); // remove the 12 first bytes int remaining = data.size(); if (remaining%7 != 0) { Log::error("ClientLobbyRoomProtocol", "ConnectionAccepted : Error in the server list"); } remaining /= 7; for (int i = 0; i < remaining; i++) { if (data[0] != 1 || data[2] != 4) Log::error("ClientLobbyRoomProtocol", "Bad format in players list."); uint8_t race_id = data[1]; uint32_t global_id = data.gui32(3); Online::OnlineProfile* new_user = new Online::OnlineProfile(global_id, ""); NetworkPlayerProfile* profile2 = new NetworkPlayerProfile(); profile2->race_id = race_id; profile2->user_profile = new_user; profile2->kart_name = ""; m_setup->addPlayer(profile2); data.removeFront(7); } // add self m_server = event->getPeer(); m_state = CONNECTED; } else Log::info("ClientLobbyRoomProtocol", "Failure during the connection acceptation process."); } // connectionAccepted
/** Sends data to all peers except the specified one. * \param peer Peer which will not receive the message. * \param data Data to sent. * \param reliable If the data should be sent reliable or now. */ void STKHost::sendPacketExcept(STKPeer* peer, const NetworkString& data, bool reliable) { for (unsigned int i = 0; i < m_peers.size(); i++) { STKPeer* p = m_peers[i]; if (!p->isSamePeer(peer)) { p->sendPacket(data, reliable); } } } // sendPacketExcept
/*! \brief Called when a player asks to select a kart. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 6 N+6 * --------------------------------------------------- * Size | 1 | 4 | 1 | N | * Data | 4 | priv token | N (kart name size) | kart name | * --------------------------------------------------- */ void ServerLobbyRoomProtocol::kartSelectionRequested(Event* event) { if(m_state!=SELECTING_KARTS) { Log::warn("Server", "Received kart selection while in state %d.", m_state); return; } const NetworkString &data = event->data(); STKPeer* peer = event->getPeer(); if (!checkDataSizeAndToken(event, 6)) return; std::string kart_name; data.decodeString(5, &kart_name); // check if selection is possible if (!m_selection_enabled) { NetworkString answer(3); // selection still not started answer.ai8(LE_KART_SELECTION_REFUSED).ai8(1).ai8(2); sendMessage(peer, answer); return; } // check if somebody picked that kart if (!m_setup->isKartAvailable(kart_name)) { NetworkString answer(3); // kart is already taken answer.ai8(LE_KART_SELECTION_REFUSED).ai8(1).ai8(0); sendMessage(peer, answer); return; } // check if this kart is authorized if (!m_setup->isKartAllowed(kart_name)) { NetworkString answer(3); // kart is not authorized answer.ai8(LE_KART_SELECTION_REFUSED).ai8(1).ai8(1); sendMessage(peer, answer); return; } // send a kart update to everyone NetworkString answer(3+1+kart_name.size()); // kart update (3), 1, race id uint8_t player_id = peer->getPlayerProfile()->getGlobalPlayerId(); answer.ai8(LE_KART_SELECTION_UPDATE).ai8(1).ai8(player_id) .encodeString(kart_name); // This message must be handled synchronously on the client. sendSynchronousMessage(answer); m_setup->setPlayerKart(player_id, kart_name); } // kartSelectionRequested
/*! \brief Called when a player asks to select a kart. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 6 N+6 * --------------------------------------------------- * Size | 1 | 4 | 1 | N | * Data | 4 | priv token | N (kart name size) | kart name | * --------------------------------------------------- */ void ServerLobbyRoomProtocol::kartSelectionRequested(Event* event) { NetworkString data = event->data(); STKPeer* peer = *(event->peer); if (!checkDataSizeAndToken(event, 6)) return; uint8_t kart_name_size = data.gui8(5); std::string kart_name = data.gs(6, kart_name_size); if (kart_name.size() != kart_name_size) { Log::error("ServerLobbyRoomProtocol", "Kart names sizes differ: told:" "%d, real: %d.", kart_name_size, kart_name.size()); return; } // check if selection is possible if (!m_selection_enabled) { NetworkString answer; answer.ai8(0x82).ai8(1).ai8(2); // selection still not started m_listener->sendMessage(this, peer, answer); return; } // check if somebody picked that kart if (!m_setup->isKartAvailable(kart_name)) { NetworkString answer; answer.ai8(0x82).ai8(1).ai8(0); // kart is already taken m_listener->sendMessage(this, peer, answer); return; } // check if this kart is authorized if (!m_setup->isKartAllowed(kart_name)) { NetworkString answer; answer.ai8(0x82).ai8(1).ai8(1); // kart is not authorized m_listener->sendMessage(this, peer, answer); return; } // send a kart update to everyone NetworkString answer; // kart update (3), 1, race id answer.ai8(0x03).ai8(1).ai8(peer->getPlayerProfile()->race_id); // kart name size, kart name answer.ai8(kart_name.size()).as(kart_name); m_listener->sendMessage(this, answer); m_setup->setPlayerKart(peer->getPlayerProfile()->race_id, kart_name); }
void ServerLobbyRoomProtocol::kartDisconnected(Event* event) { STKPeer* peer = *(event->peer); if (peer->getPlayerProfile() != NULL) // others knew him { NetworkString msg; msg.ai8(0x02).ai8(1).ai8(peer->getPlayerProfile()->race_id); m_listener->sendMessage(this, msg); Log::info("ServerLobbyRoomProtocol", "Player disconnected : id %d", peer->getPlayerProfile()->race_id); m_setup->removePlayer(peer->getPlayerProfile()->race_id); NetworkManager::getInstance()->removePeer(peer); } else Log::info("ServerLobbyRoomProtocol", "The DC peer wasn't registered."); }
// ---------------------------------------------------------------------------- bool StartGameProtocol::notifyEventAsynchronous(Event* event) { const NetworkString &data = event->data(); if (data.size() < 5) { Log::error("StartGameProtocol", "Too short message."); return true; } uint32_t token = data.gui32(); uint8_t ready = data.gui8(4); STKPeer* peer = event->getPeer(); if (peer->getClientServerToken() != token) { Log::error("StartGameProtocol", "Bad token received."); return true; } if (NetworkConfig::get()->isServer() && ready) // on server, player is ready { Log::info("StartGameProtocol", "One of the players is ready."); m_player_states[peer->getPlayerProfile()] = READY; m_ready_count++; if (m_ready_count == m_game_setup->getPlayerCount()) { // everybody ready, synchronize Protocol *p = ProtocolManager::getInstance() ->getProtocol(PROTOCOL_SYNCHRONIZATION); SynchronizationProtocol* protocol = static_cast<SynchronizationProtocol*>(p); if (protocol) { protocol->startCountdown(5000); // 5 seconds countdown Log::info("StartGameProtocol", "All players ready, starting countdown."); m_ready = true; return true; } else Log::error("StartGameProtocol", "The Synchronization protocol hasn't been started."); } // if m_ready_count == number of players } else // on the client, we shouldn't even receive messages. { Log::error("StartGameProtocol", "Received a message with bad format."); } return true; } // notifyEventAsynchronous
/*! \brief Called when a player votes for the number of races in a GP. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 6 7 * ------------------------------------ * Size | 1 | 4 | 1 | 1 | * Data | 4 | priv token | 1 | races count | * ------------------------------------ */ void ServerLobbyRoomProtocol::playerRaceCountVote(Event* event) { NetworkString &data = event->data(); STKPeer* peer = event->getPeer(); if (!checkDataSizeAndToken(event, 7)) return; if (!isByteCorrect(event, 5, 1)) return; uint8_t player_id = peer->getPlayerProfile()->getGlobalPlayerId(); m_setup->getRaceConfig()->setPlayerRaceCountVote(player_id, data[6]); // Send the vote to everybody (including the sender) data.removeFront(5); // remove the token NetworkString other(2+data.size()); other.ai8(1).ai8(player_id); // add the player id other += data; // add the data sendMessageToPeersChangingToken(LE_VOTE_RACE_COUNT, other); } // playerRaceCountVote
/*! \brief Called when a player votes for a minor race mode. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 6 7 * ---------------------------------------- * Size | 1 | 4 | 1 | 1 | * Data | 4 | priv token | 1 | minor mode vote | * ---------------------------------------- */ void ServerLobbyRoomProtocol::playerMinorVote(Event* event) { NetworkString data = event->data(); STKPeer* peer = *(event->peer); if (!checkDataSizeAndToken(event, 7)) return; if (!isByteCorrect(event, 5, 1)) return; uint8_t player_id = peer->getPlayerProfile()->race_id; m_setup->getRaceConfig()->setPlayerMinorVote(player_id, data[6]); // Send the vote to everybody (including the sender) NetworkString other; other.ai8(1).ai8(player_id); // add the player id data.removeFront(5); // remove the token other += data; // add the data NetworkString prefix; prefix.ai8(0xc2); // prefix the token with the ype sendMessageToPeersChangingToken(prefix, other); }
/*! \brief Called when a player votes for a track. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 6 N+6 N+7 N+8 * ----------------------------------------------------------- * Size | 1 | 4 | 1 | N | 1 | 1 | * Data | 4 | priv token | N | track name | 1 | track number (gp) | * ----------------------------------------------------------- */ void ServerLobbyRoomProtocol::playerTrackVote(Event* event) { NetworkString &data = event->data(); STKPeer* peer = event->getPeer(); if (!checkDataSizeAndToken(event, 8)) return; std::string track_name; int N = data.decodeString(5, &track_name); if (!isByteCorrect(event, N+5, 1)) return; uint8_t player_id = peer->getPlayerProfile()->getGlobalPlayerId(); m_setup->getRaceConfig()->setPlayerTrackVote(player_id, track_name, data[N+6]); // Send the vote to everybody (including the sender) data.removeFront(5); // remove the token NetworkString other(2+data.size()); other.ai8(1).ai8(player_id); // add the player id other += data; // add the data sendMessageToPeersChangingToken(LE_VOTE_TRACK, other); if(m_setup->getRaceConfig()->getNumTrackVotes()==m_setup->getPlayerCount()) startGame(); } // playerTrackVote
void ServerLobbyRoomProtocol::kartDisconnected(Event* event) { STKPeer* peer = event->getPeer(); if (peer->getPlayerProfile() != NULL) // others knew him { NetworkString msg(3); msg.ai8(LE_PLAYER_DISCONNECTED).ai8(1) .ai8(peer->getPlayerProfile()->getGlobalPlayerId()); sendMessage(msg); Log::info("ServerLobbyRoomProtocol", "Player disconnected : id %d", peer->getPlayerProfile()->getGlobalPlayerId()); m_setup->removePlayer(peer->getPlayerProfile()); // Remove the profile from the peer (to avoid double free) peer->setPlayerProfile(NULL); STKHost::get()->removePeer(peer); } else Log::info("ServerLobbyRoomProtocol", "The DC peer wasn't registered."); } // kartDisconnected
/*! \brief Called when a player asks for a connection. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 * --------------------- * Size | 1 |1| | * Data | 4 |n| player name | * --------------------- */ void ServerLobbyRoomProtocol::connectionRequested(Event* event) { STKPeer* peer = event->getPeer(); const NetworkString &data = event->data(); // can we add the player ? if (m_setup->getPlayerCount() >= NetworkConfig::get()->getMaxPlayers() || m_state!=ACCEPTING_CLIENTS ) { NetworkString *message = getNetworkString(2); // Len, error code: 2 = busy, 0 = too many players message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(m_state!=ACCEPTING_CLIENTS ? 2 : 0); // send only to the peer that made the request peer->sendPacket(message); delete message; Log::verbose("ServerLobbyRoomProtocol", "Player refused"); return; } // Connection accepted. // ==================== std::string name_u8; int len = data.decodeString(&name_u8); core::stringw name = StringUtils::utf8ToWide(name_u8); std::string password; data.decodeString(&password); bool is_authorised = (password==NetworkConfig::get()->getPassword()); // Get the unique global ID for this player. m_next_player_id.lock(); m_next_player_id.getData()++; int new_player_id = m_next_player_id.getData(); m_next_player_id.unlock(); if(m_setup->getLocalMasterID()==0) m_setup->setLocalMaster(new_player_id); // The host id has already been incremented when the peer // was added, so it is the right id now. int new_host_id = STKHost::get()->getNextHostId(); // Notify everybody that there is a new player // ------------------------------------------- NetworkString *message = getNetworkString(3+1+name_u8.size()); // size of id -- id -- size of local id -- local id; message->addUInt8(LE_NEW_PLAYER_CONNECTED).addUInt8(new_player_id) .addUInt8(new_host_id).encodeString(name_u8); STKHost::get()->sendPacketExcept(peer, message); delete message; // Now answer to the peer that just connected // ------------------------------------------ RandomGenerator token_generator; // use 4 random numbers because rand_max is probably 2^15-1. uint32_t token = (uint32_t)((token_generator.get(RAND_MAX) & 0xff) << 24 | (token_generator.get(RAND_MAX) & 0xff) << 16 | (token_generator.get(RAND_MAX) & 0xff) << 8 | (token_generator.get(RAND_MAX) & 0xff)); peer->setClientServerToken(token); peer->setAuthorised(is_authorised); peer->setHostId(new_host_id); const std::vector<NetworkPlayerProfile*> &players = m_setup->getPlayers(); // send a message to the one that asked to connect // Estimate 10 as average name length NetworkString *message_ack = getNetworkString(4 + players.size() * (2+10)); // connection success -- size of token -- token message_ack->addUInt8(LE_CONNECTION_ACCEPTED).addUInt8(new_player_id) .addUInt8(new_host_id).addUInt8(is_authorised); // Add all players so that this user knows (this new player is only added // to the list of players later, so the new player's info is not included) for (unsigned int i = 0; i < players.size(); i++) { message_ack->addUInt8(players[i]->getGlobalPlayerId()) .addUInt8(players[i]->getHostId()) .encodeString(players[i]->getName()); } peer->sendPacket(message_ack); delete message_ack; NetworkPlayerProfile* profile = new NetworkPlayerProfile(name, new_player_id, new_host_id); m_setup->addPlayer(profile); NetworkingLobby::getInstance()->addPlayer(profile); Log::verbose("ServerLobbyRoomProtocol", "New player."); } // connectionRequested
/** \brief Thread function checking if data is received. * This function tries to get data from network low-level functions as * often as possible. When something is received, it generates an * event and passes it to the Network Manager. * \param self : used to pass the ENet host to the function. */ void* STKHost::mainLoop(void* self) { VS::setThreadName("STKHost"); ENetEvent event; STKHost* myself = (STKHost*)(self); ENetHost* host = myself->m_network->getENetHost(); if(NetworkConfig::get()->isServer() && NetworkConfig::get()->isLAN() ) { TransportAddress address(0, 2757); ENetAddress eaddr = address.toEnetAddress(); myself->m_lan_network = new Network(1, 1, 0, 0, &eaddr); } while (!myself->mustStopListening()) { if(myself->m_lan_network) { myself->handleLANRequests(); } // if discovery host while (enet_host_service(host, &event, 20) != 0) { if (event.type == ENET_EVENT_TYPE_NONE) continue; // Create an STKEvent with the event data. This will also // create the peer if it doesn't exist already Event* stk_event = new Event(&event); if (stk_event->getType() == EVENT_TYPE_MESSAGE) Network::logPacket(stk_event->data(), true); Log::verbose("STKHost", "Event of type %d received", (int)(stk_event->getType())); STKPeer* peer = stk_event->getPeer(); if (stk_event->getType() == EVENT_TYPE_CONNECTED) { Log::info("STKHost", "A client has just connected. There are " "now %lu peers.", myself->m_peers.size()); Log::debug("STKHost", "Addresses are : %lx, %lx", stk_event->getPeer(), peer); } // EVENT_TYPE_CONNECTED else if (stk_event->getType() == EVENT_TYPE_MESSAGE) { TransportAddress stk_addr(peer->getAddress()); Log::verbose("NetworkManager", "Message, Sender : %s, message = \"%s\"", stk_addr.toString(/*show port*/false).c_str(), stk_event->data().std_string().c_str()); } // if message event // notify for the event now. ProtocolManager::getInstance()->propagateEvent(stk_event); } // while enet_host_service } // while !mustStopListening free(myself->m_listening_thread); myself->m_listening_thread = NULL; Log::info("STKHost", "Listening has been stopped"); return NULL; } // mainLoop
/*! \brief Called when a player asks for a connection. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 5 * ------------------------ * Size | 1 | 4 | * Data | 4 | global player id | * ------------------------ */ void ServerLobbyRoomProtocol::connectionRequested(Event* event) { STKPeer* peer = *(event->peer); NetworkString data = event->data(); if (data.size() != 5 || data[0] != 4) { Log::warn("ServerLobbyRoomProtocol", "Receiving badly formated message. Size is %d and first byte %d", data.size(), data[0]); return; } uint32_t player_id = 0; player_id = data.getUInt32(1); // can we add the player ? if (m_setup->getPlayerCount() < ServerNetworkManager::getInstance()->getMaxPlayers()) //accept { // add the player to the game setup m_next_id = m_setup->getPlayerCount(); // notify everybody that there is a new player NetworkString message; // new player (1) -- size of id -- id -- size of local id -- local id; message.ai8(1).ai8(4).ai32(player_id).ai8(1).ai8(m_next_id); m_listener->sendMessageExcept(this, peer, message); /// now answer to the peer that just connected RandomGenerator token_generator; // use 4 random numbers because rand_max is probably 2^15-1. uint32_t token = (uint32_t)(((token_generator.get(RAND_MAX)<<24) & 0xff) + ((token_generator.get(RAND_MAX)<<16) & 0xff) + ((token_generator.get(RAND_MAX)<<8) & 0xff) + ((token_generator.get(RAND_MAX) & 0xff))); // send a message to the one that asked to connect NetworkString message_ack; // connection success (129) -- size of token -- token message_ack.ai8(0x81).ai8(1).ai8(m_next_id).ai8(4).ai32(token).ai8(4).ai32(player_id); // add all players so that this user knows std::vector<NetworkPlayerProfile*> players = m_setup->getPlayers(); for (unsigned int i = 0; i < players.size(); i++) { // do not duplicate the player into the message if (players[i]->race_id != m_next_id && players[i]->user_profile->getID() != player_id) message_ack.ai8(1).ai8(players[i]->race_id).ai8(4).ai32(players[i]->user_profile->getID()); } m_listener->sendMessage(this, peer, message_ack); peer->setClientServerToken(token); NetworkPlayerProfile* profile = new NetworkPlayerProfile(); profile->race_id = m_next_id; profile->kart_name = ""; profile->user_profile = new Online::OnlineProfile(player_id, ""); m_setup->addPlayer(profile); peer->setPlayerProfile(profile); Log::verbose("ServerLobbyRoomProtocol", "New player."); } // accept player else // refuse the connection with code 0 (too much players) { NetworkString message; message.ai8(0x80); // 128 means connection refused message.ai8(1); // 1 bytes for the error code message.ai8(0); // 0 = too much players // send only to the peer that made the request m_listener->sendMessage(this, peer, message); Log::verbose("ServerLobbyRoomProtocol", "Player refused"); } }