/* net/netconnection.cc This file is part of the Osirion project and is distributed under the terms of the GNU General Public License version 2 */ #include #include #include #include "sys/sys.h" #include "core/application.h" #include "core/gameconnection.h" #include "core/netconnection.h" #include "core/player.h" #include "core/stats.h" namespace core { NetConnection::NetConnection() { connection_timeout = core::application()->time(); connection_state = Connecting; connection_timestamp = 0; receive_compressed = false; received_compressed_size = 0; compressed_size = 0; } NetConnection::~NetConnection() { disconnect(); } void NetConnection::abort() { connection_error = true; } void NetConnection::connect(std::string const &to_host, int to_port) { connection_error = false; connection_fd = -1; connection_state = Connecting; if (valid()) return; // resolve serverhostname struct hostent *serverhostent; serverhostent = gethostbyname(to_host.c_str()); if (!serverhostent) { con_warn << "Could not resolve '" << to_host.c_str() << "'" << std::endl; abort(); return; } // Get a socket file descriptor connection_fd = socket(PF_INET, SOCK_DGRAM, 0); if (connection_fd == -1) { //con_error << "Network socket() failed!" << std::endl; abort(); return; } // make the connection server_addr.sin_family = AF_INET; server_addr.sin_port = htons(to_port); // FIXME inet_addr can still fail server_addr.sin_addr.s_addr = inet_addr(inet_ntoa(*((struct in_addr *)serverhostent->h_addr))); memset(server_addr.sin_zero, '\0', sizeof server_addr.sin_zero); if (server_addr.sin_addr.s_addr == INADDR_NONE) { con_error << "Network invalid address " << to_host << "!" << std::endl; abort(); return; } connection_host = to_host; connection_port = to_port; connection_error = false; FD_ZERO(&clientset); #ifdef _WIN32 FD_SET((unsigned int) fd(), &clientset); #else FD_SET(fd(), &clientset); #endif connection_timeout = application()->time(); connection_keepalive = application()->time(); connection_state = Pending; game()->localplayer()->set_dirty(); std::stringstream str; str << "Connecting to " << inet_ntoa(*((struct in_addr *)serverhostent->h_addr)) << ":" << to_port << "..."; con_print << str.str() << std::endl; application()->notify_loader(str.str()); } void NetConnection::disconnect() { if (connection_fd != -1) { sendq.clear(); sendq.assign("disconnect\n"); transmit(); FD_ZERO(&clientset); #ifdef _WIN32 closesocket(connection_fd); #else close(connection_fd); #endif } connection_fd = -1; connection_error = false; connection_host.clear(); connection_port = 0; connection_state = Connecting; } bool NetConnection::has_messages() const { return (recvq.size() > 0); } void NetConnection::retreive(std::string & message) { if (recvq.size() > 0) { message.assign(recvq.front()); recvq.pop_front(); } else { message.clear(); } } // receive data and decode it into lines void NetConnection::receive() { // TODO: binary mode data transfer if (error() || invalid()) return; ssize_t bytes_received; memset(recvbuf, 0, BLOCKSIZE); bytes_received = ::recv(connection_fd, recvbuf, BLOCKSIZE - 1, 0); Stats::network_bytes_received += bytes_received; connection_timeout = core::application()->time(); if (bytes_received == 0) { con_print << "^BDisconnected." << std::endl; abort(); return; } else if (bytes_received < 0) { con_error << "Network receive() error!" << std::endl; //perror("recv"); abort(); return; } const char *c = recvbuf; while (bytes_received) { if (receive_compressed) { zrecvbuf[received_compressed_size] = *c; received_compressed_size++; if (compressed_size == received_compressed_size) { // uncompress char zunbuf[BLOCKSIZE*2]; memset(zunbuf, 0, sizeof(zunbuf)); uLong zunbuf_size = (uLong) BLOCKSIZE * 2 - 1; int status = uncompress((Bytef *) zunbuf, &zunbuf_size, (Bytef *) zrecvbuf, (uLong) compressed_size); if (status != Z_OK) { con_warn << "zlib error " << status << " uncompressing incoming datablock!\n"; } else { const char *zc = zunbuf; while (*zc) { if ((*zc == '\n') || (*zc == '\r')) { if (messageblock.size()) { recvq.push_back(messageblock); messageblock.clear(); } } else { if (messageblock.size() < FRAMESIZE) { messageblock += *zc; } else { con_warn << "Incoming uncompressed message exceeds " << FRAMESIZE << " bytes!\n"; messageblock.clear(); } } zc++; } } // reset receive_compressed = false; received_compressed_size = 0; compressed_size = 0; } } else if (!messageblock.size() && (bytes_received > 3) && (*c == '\xff') && (*(c + 1) == '\xff')) { receive_compressed = true; received_compressed_size = 0; compressed_size = (uLong)(*(unsigned char *)(c + 2) + (*(unsigned char *)(c + 3) << 8)); c += 3; bytes_received -= 3; memset(zrecvbuf, 0, sizeof(zrecvbuf)); if (compressed_size > BLOCKSIZE) { con_warn << "Incoming compressed datablock exceeds " << BLOCKSIZE << " bytes!\n"; receive_compressed = false; } } else if ((*c == '\n') || (*c == '\r')) { if (messageblock.size()) { recvq.push_back(messageblock); messageblock.clear(); } } else { if (messageblock.size() < FRAMESIZE) { messageblock += *c; } else { con_warn << "Incoming message exceeds " << FRAMESIZE << " bytes!\n"; messageblock.clear(); } } c++; bytes_received--; } } void NetConnection::frame() { timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 0; fd_set readset = clientset; int nb = select(fd() + 1, &readset, NULL, NULL, &timeout); if (nb == 0) { if (connection_timeout + NETTIMEOUT < core::application()->time()) { con_error << "Connection timeout!\n"; abort(); } return; } if (nb == -1) { con_error << "Network error on select()" << std::endl; //perror("select"); abort(); } while (FD_ISSET(this->fd(), &readset) && !error()) { receive(); while (has_messages()) { std::string message; retreive(message); parse_incoming_message(message); } nb = select(fd() + 1, &readset, NULL, NULL, &timeout); if (nb == 0) { return; } if (nb == -1) { con_error << "Network error on select()" << std::endl; //perror("select"); abort(); } } } // transmit all queued messages void NetConnection::transmit() { if (error() || invalid()) return; if (!sendq.size()) { if (connection_keepalive + NETTIMEOUT / 2 < application()->time()) { sendq.assign("ping\n"); } else { return; } } else if (sendq.size() >= BLOCKSIZE - 16) { con_warn << "Outgoing data exceeds " << BLOCKSIZE - 16 << " bytes!\n"; sendq.clear(); return; } ssize_t bytes_sent = 0; while (sendq.size()) { bytes_sent = ::sendto(connection_fd, sendq.c_str(), sendq.size(), 0, (struct sockaddr *) & server_addr, sizeof(server_addr)); if (bytes_sent <= 0) { con_error << "Network send() error!" << std::endl; //perror("send"); abort(); return; } // assert (bytes_sent <= sendbuf.size()); sendq.erase(0, bytes_sent); Stats::network_bytes_sent += bytes_sent; } connection_keepalive = application()->time(); } // queue a mmessage to the server void NetConnection::send_raw(std::string const &msg) { sendq.append(msg); } // functions for outgoing messages /** * the following outgoing messages can be send * * connect * pif * cup * cmd * say * priv */ // send a "connect" message to the server void NetConnection::send_connect() { std::ostringstream msg; msg << "connect " << PROTOCOLVERSION << "\n"; this->send_raw(msg.str()); } // send a "pif" player info message to the server void NetConnection::send_playerinfo() { localplayer()->update_info(); std::ostringstream msg; msg << "pif "; localplayer()->serialize_client_update(msg); msg << '\n'; this->send_raw(msg.str()); localplayer()->set_dirty(false); } // send a "cup" client update message to the server void NetConnection::send_clientupdate(Entity *entity) { // cup std::ostringstream msg; msg << "cup " << entity->id() << " "; entity->serialize_client_update(msg); msg << '\n'; this->send_raw(msg.str()); } // send a "cmd" command line message to the server void NetConnection::send_command(std::string const &cmdline) { std::string msg("cmd "); msg.append(cmdline); msg += '\n'; this->send_raw(msg); } // send a "rcon" command line message to the server void NetConnection::send_rcon(std::string const &cmdline) { std::string msg("rcon "); msg.append(cmdline); msg += '\n'; this->send_raw(msg); } // send a "say" chat message message to the server void NetConnection::send_say(std::string const &text) { std::string msg("say "); msg.append(text); msg += '\n'; this->send_raw(msg); } // send a "priv" private message to the server void NetConnection::send_private_message(std::string const &text) { std::string msg("priv "); msg.append(text); msg += '\n'; this->send_raw(msg); } // send a ping reply void NetConnection::send_ping_reply(unsigned long timestamp) { std::ostringstream msg; msg << "ping " << timestamp << '\n'; this->send_raw(msg.str()); } // send an info record request void NetConnection::send_info_request(Info *info) { std::ostringstream msg; msg << "inf " << '"' << info->label() << '"' << '\n'; this->send_raw(msg.str()); info->set_timestamp(application()->timestamp()); } // parse incoming client messages /** * The following incoming messages are parsed; * * connect * disconnect * msg info * msg public * msg private * msg rcon * msg snd * die * ent * frame * sup * pif * zone */ void NetConnection::parse_incoming_message(const std::string & message) { std::istringstream msgstream(message); std::string command; msgstream >> command; if (command == "msg") { std::string level; if (msgstream >> level) { if (level == "info") { if (message.size() > 9) { application()->notify_message(Message::Info, message.substr(9)); } } else if (level == "rcon") { if (message.size() > 9) { application()->notify_message(Message::RCon, message.substr(9)); } } else if (level == "public") { // FIXME - separate sender nickname if (message.size() > 11) { application()->notify_message(Message::Public, message.substr(11)); } } else if (level == "private") { // FIXME - separate sender nickname if (message.size() > 12) { application()->notify_message(Message::Private, message.substr(12)); } } else if (level == "snd") { if (message.size() > 8) { application()->notify_sound(message.substr(8).c_str()); } } } } else if (command == "connect") { if (connection_state == Pending) { send_playerinfo(); connection_state = Connected; con_print << "^BConnected." << std::endl; } return; } else if (command == "disconnect") { con_error << "Server disconnected!" << std::endl; abort(); } else if (command == "ping") { unsigned long timestamp; if ((msgstream >> timestamp)) { send_ping_reply(timestamp); } } else if (command == "frame") { unsigned long timestamp; if ((msgstream >> timestamp)) { send_ping_reply(timestamp); connection_timestamp = timestamp; } } else if (command == "die") { unsigned int id; if (msgstream >> id) { //con_debug << "Received die entity id " << id << std::endl; Entity *e = Entity::find(id); if (localcontrol() == e) localplayer()->set_control(0); if (e) Entity::erase(id); } } else if (command == "ent") { unsigned int type = 0; unsigned int id = 0; if ((msgstream >> id) && (msgstream >> type)) { if (!id) { con_warn << "Received create for NULL entity!" << std::endl; return; } //con_debug << "Received create entity id " << id << " type " << type << std::endl; Entity *entity = Entity::find(id); if (!entity) { switch (type) { case Entity::Default: entity = new Entity(msgstream); break; case Entity::Dynamic: entity = new EntityDynamic(msgstream); break; case Entity::Controlable: entity = new EntityControlable(msgstream); break; case Entity::Globe: entity = new EntityGlobe(msgstream); break; default: con_warn << "Received create for unknown entity type " << type << "!" << std::endl; return; break; } Entity::add(entity, id); } entity->receive_server_create(msgstream); //game()->update_entity_clientstate(entity); } } else if (command == "menu") { unsigned int id = 0; if (msgstream >> id) { if (!id) { con_warn << "Received menu for NULL entity!" << std::endl; return; } Entity *entity = Entity::find(id); if (!entity) { con_warn << "Received menu for unknown entity " << id << "!" << std::endl; return; } MenuDescription *menu = Descriptions::receive(msgstream); if (!menu->label().size()) { con_warn << "Received menu without label for entity " << id << "!" << std::endl; delete menu; return; } // remove the menu if it already exists entity->remove_menu(menu->label()); //con_debug << "receiving menu " << entity->label() << " " << menu->label() << std::endl; entity->add_menu(menu); } else { con_warn << "Received illegal menu message!" << std::endl; } } else if (command.compare("zone") == 0) { unsigned int id; std::string label; if (msgstream >> id) { if (id) { //con_debug << "Received zone " << id << std::endl; Zone * zone = Zone::find(id); // create the zone if necessary if (!zone) { zone = new Zone(msgstream); Zone::add(zone, id); } else { zone->receive_server_update(msgstream); } } // control is set to 0 on zone change and is put back by pif connection()->localplayer()->set_control(0); } } else if (command == "pif") { //con_debug << "Received update player info" << std::endl; int player_id; if (!(msgstream >> player_id)) { con_warn << "Received illegal player info message!" << std::endl; return; } // normal "pif" message about localplayer if (!player_id) { Zone *oldzone = connection()->localplayer()->zone(); connection()->localplayer()->receive_server_update(msgstream); //con_debug << "zone " << ( connection()->localplayer()->zone() ? connection()->localplayer()->zone()->id() : 0) << std::endl; if (connection()->localplayer()->zonechange() && oldzone && (oldzone != connection()->localplayer()->zone())) { // notify the applciation to clear none-core zone assets (textures etc) application()->notify_zonechange(); // delete all entities in the old zone for (Entity::Registry::iterator it = Entity::registry().begin(); it != Entity::registry().end();) { Entity *entity = (*it).second; if ((entity->zone() == oldzone)) { delete entity; Entity::registry().erase(it++); } else { ++it; } } oldzone->content().clear(); } // short "pif" message about a different player } else if (player_id != localplayer()->id()) { // find player Player *player = 0; // search other players for (GameInterface::Players::iterator it = game()->players().begin(); it != game()->players().end() && !player; it++) { if ((*it)->id() == player_id) { player = (*it); } } if (!player) { player = new Player(); game()->players().push_back(player); } player->receive_short_server_update(msgstream); player->set_dirty(false); } } else if (command == "pid") { con_debug << "Received player disconnect info" << std::endl; int player_id; if (!(msgstream >> player_id)) { con_warn << "Received illegal player disconnect message!" << std::endl; return; } // find player if (player_id == connection()->localplayer()->id()) { // ignore disconnect messages for local client return; } // search other players Player *player = 0; for (GameInterface::Players::iterator it = game()->players().begin(); it != game()->players().end() && !player; it++) { if ((*it)->id() == player_id) { game()->players().erase(it); return; } } } else if (command == "inf") { // incoming info record std::string label; char c; while ((msgstream.get(c)) && (c != '"')); while ((msgstream.get(c)) && (c != '"')) label += c; if (label.size()) { Info *info = Info::find(label); if (!info) { info = new Info(label); Info::add(info); } info->receive_server_update(msgstream); info->clear_timestamp(); } else { con_warn << "Received empty information record!" << std::endl; } } else if (command == "sup") { if (connection_state == Connected) { unsigned int id; if (msgstream >> id) { //con_debug << "Received update entity id " << id << std::endl; Entity *entity = Entity::find(id); if (!entity) { // FIXME request entity from the server con_warn << "Update for unknown entity " << id << "!" << std::endl; } else { // FIXME check of the received update matches the actual entity entity->receive_server_update(msgstream); } } } } } }