/* 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_client_update(Entity *entity) { // cup std::ostringstream msg; msg << "cup " << entity->id() << " "; entity->serialize_client_update(msg); msg << '\n'; this->send_raw(msg.str()); } // send a "req" entity request void NetConnection::send_entity_request(Entity *entity) { // req std::ostringstream msg; msg << "req " << entity->id() << '\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 // ping timestamp 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 // inf type.label info.label void NetConnection::send_info_request(Info *info) { std::ostringstream msg; if (!info->id()) { return; } msg << "inf " << info->id() << "\n"; this->send_raw(msg.str()); info->set_timestamp(application()->timestamp()); } // send an inventory update request void NetConnection::send_inventory_request(Entity *entity) { if (!entity || !entity->inventory()) return; std::ostringstream msg; msg << "inv " << entity->id() << " " << entity->inventory()->timestamp() << "\n"; this->send_raw(msg.str()); } // 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 * pid * inf * inv * zone */ void NetConnection::parse_incoming_message(const std::string & message) { std::istringstream msgstream(message); std::string command; msgstream >> command; if (command.compare("sup") == 0) { // entity server update: sup if (connection_state == Connected) { unsigned int type; unsigned int id; if ((msgstream >> id) && (msgstream >> type)) { if (!id) { return; } Entity *entity = Entity::find(id); if (entity) { // validate entity type if (entity->type() != type) { // type mismatch, delete the entity if (localcontrol() == entity) localplayer()->set_control(0); Entity::erase(id); entity = 0; } } 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 update for unknown entity type " << type << "!" << std::endl; return; break; } Entity::add(entity, id); send_entity_request(entity); } // receive update entity->receive_server_update(msgstream); } } } else if (command.compare("ent") == 0) { // entity create message 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) { // validate entity type if (entity->type() != type) { // type mismatch, delete the entity if (localcontrol() == entity) localplayer()->set_control(0); Entity::erase(id); entity = 0; } } 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); } } else if (command.compare("die") == 0) { 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.compare("msg") == 0) { // text message: 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.compare("connect") == 0) { if (connection_state == Pending) { send_playerinfo(); connection_state = Connected; con_print << "^BConnected." << std::endl; } return; } else if (command.compare("disconnect") == 0) { con_error << "Server disconnected!" << std::endl; abort(); } else if (command.compare("ping") == 0) { unsigned long timestamp; if ((msgstream >> timestamp)) { send_ping_reply(timestamp); } } else if (command.compare("frame") == 0) { unsigned long timestamp; if ((msgstream >> timestamp)) { send_ping_reply(timestamp); connection_timestamp = timestamp; } } else if (command.compare("menu") == 0) { 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.compare("pif") == 0) { //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.compare("pid") == 0) { //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.compare("inf") == 0) { // incoming info record unsigned int id = 0; std::string typelabelstr; std::string infolabelstr; std::string n; char c; // read id if (!(msgstream >> id)) { con_warn << "Received invalid info record message!" << std::endl; return; } if (id == 0) { // special case: info type constructor size_t s; if (!(msgstream >> s)) { con_warn << "Received invalid info type message!" << std::endl; return; } for (size_t i = 0; i < s; i++) { // read infotype label n.clear(); while ((msgstream.get(c)) && (c != '"')); while ((msgstream.get(c)) && (c != '"')) n +=c; if (n.size() && !InfoType::find(n)) { // add new infotype new InfoType(n.c_str()); } } return; } // read type label n.clear(); while ((msgstream.get(c)) && (c != '"')); while ((msgstream.get(c)) && (c != '"')) n +=c; typelabelstr.assign(n); if (!typelabelstr.size()) { con_warn << "Received invalid info record message!" << std::endl; return; } // read info label n.clear(); while ((msgstream.get(c)) && (c != '"')); while ((msgstream.get(c)) && (c != '"')) n +=c; infolabelstr.assign(n); if (!infolabelstr.size()) { con_warn << "Received invalid info record message for type '" << typelabelstr << "'!" << std::endl; return; } // find the InfoType instance InfoType *infotype = InfoType::find(typelabelstr); if (!infotype) { infotype = new InfoType(typelabelstr.c_str()); } // find the Info instance Info *info = Info::find(id); if (!info) { info = Info::find(infotype, infolabelstr); } // create one if necessary if (!info) { info = new Info(id); } info->set_type(infotype); info->set_label(infolabelstr); info->set_id(id); info->receive_server_update(msgstream); //info->clear_timestamp(); info->set_timestamp(application()->timestamp()); //con_debug << "CLIENT info for " << info->id() << " " << info->type()->label() << ":" << info->label() << " timestamp " << info->timestamp() << std::endl; } else if (command.compare("inv") == 0) { // received inventory update unsigned int id = 0; unsigned long server_timestamp; // read id if (!(msgstream >> id >> server_timestamp)) { con_warn << "Received invalid inventory update message!" << std::endl; return; } Entity *entity = Entity::find(id); if (!entity) { con_warn << "Received inventory update for non-existing entity " << id << "!" << std::endl; return; } if (!entity->inventory()) { con_warn << "Received inventory update for entity " << id << " without inventory!" << std::endl; return; } //con_debug << "CLIENT received inv message for entity " << id << " client timestamp " << entity->inventory()->timestamp() << " server timestamp " << server_timestamp << std::endl; if (server_timestamp < entity->inventory()->timestamp()) return; size_t nbitems = 0; if (!(msgstream >> nbitems)) nbitems = 0; //con_debug << "CLIENT number of items: " << nbitems << std::endl; for (size_t i = 0; i < nbitems; i++) { if (!(msgstream >> id)) { con_warn << "Received inventory update without info id for existing entity " << id << "!" << std::endl; return; } Info *info = core::game()->request_info(id); Item *item = entity->inventory()->find(info); if (!item) { item = new Item(info); entity->inventory()->add(item); } item->receive_server_update(msgstream); } entity->inventory()->recalculate(); entity->inventory()->set_timestamp(server_timestamp); //con_debug << "CLIENT inventory updated timestamp " << entity->inventory()->timestamp() << " for " << nbitems << " items" << std::endl; } } }