/* Minetest Copyright (C) 2013 celeron55, Perttu Ahola This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include "client.h" #include "network/clientopcodes.h" #include "network/connection.h" #include "network/networkpacket.h" #include "threading/mutex_auto_lock.h" #include "client/clientevent.h" #include "client/gameui.h" #include "client/renderingengine.h" #include "client/tile.h" #include "util/auth.h" #include "util/directiontables.h" #include "util/pointedthing.h" #include "util/serialize.h" #include "util/string.h" #include "util/srp.h" #include "filesys.h" #include "mapblock_mesh.h" #include "mapblock.h" #include "minimap.h" #include "modchannels.h" #include "mods.h" #include "profiler.h" #include "shader.h" #include "gettext.h" #include "clientmap.h" #include "clientmedia.h" #include "version.h" #include "database/database-sqlite3.h" #include "serialization.h" #include "guiscalingfilter.h" #include "script/scripting_client.h" #include "game.h" #include "chatmessage.h" #include "translation.h" extern gui::IGUIEnvironment* guienv; /* Client */ Client::Client( const char *playername, const std::string &password, const std::string &address_name, MapDrawControl &control, IWritableTextureSource *tsrc, IWritableShaderSource *shsrc, IWritableItemDefManager *itemdef, NodeDefManager *nodedef, ISoundManager *sound, MtEventManager *event, bool ipv6, GameUI *game_ui ): m_tsrc(tsrc), m_shsrc(shsrc), m_itemdef(itemdef), m_nodedef(nodedef), m_sound(sound), m_event(event), m_mesh_update_thread(this), m_env( new ClientMap(this, control, 666), tsrc, this ), m_particle_manager(&m_env), m_con(new con::Connection(PROTOCOL_ID, 512, CONNECTION_TIMEOUT, ipv6, this)), m_address_name(address_name), m_server_ser_ver(SER_FMT_VER_INVALID), m_last_chat_message_sent(time(NULL)), m_password(password), m_chosen_auth_mech(AUTH_MECHANISM_NONE), m_media_downloader(new ClientMediaDownloader()), m_state(LC_Created), m_game_ui(game_ui), m_modchannel_mgr(new ModChannelMgr()) { // Add local player m_env.setLocalPlayer(new LocalPlayer(this, playername)); if (g_settings->getBool("enable_minimap")) { m_minimap = new Minimap(this); } m_cache_save_interval = g_settings->getU16("server_map_save_interval"); m_modding_enabled = g_settings->getBool("enable_client_modding"); m_script = new ClientScripting(this); m_env.setScript(m_script); m_script->setEnv(&m_env); } void Client::loadBuiltin() { // Load builtin scanModIntoMemory(BUILTIN_MOD_NAME, getBuiltinLuaPath()); m_script->loadModFromMemory(BUILTIN_MOD_NAME); } void Client::loadMods() { // Don't permit to load mods twice if (m_mods_loaded) { return; } // If modding is not enabled or flavour disable it, don't load mods, just builtin if (!m_modding_enabled) { warningstream << "Client side mods are disabled by configuration." << std::endl; return; } if (checkCSMFlavourLimit(CSMFlavourLimit::CSM_FL_LOAD_CLIENT_MODS)) { warningstream << "Client side mods are disabled by server." << std::endl; // If mods loading is disabled and builtin integrity is wrong, disconnect user. if (!checkBuiltinIntegrity()) { // @TODO disconnect user } return; } ClientModConfiguration modconf(getClientModsLuaPath()); m_mods = modconf.getMods(); // complain about mods with unsatisfied dependencies if (!modconf.isConsistent()) { modconf.printUnsatisfiedModsError(); } // Print mods infostream << "Client Loading mods: "; for (const ModSpec &mod : m_mods) infostream << mod.name << " "; infostream << std::endl; // Load and run "mod" scripts for (const ModSpec &mod : m_mods) { if (!string_allowed(mod.name, MODNAME_ALLOWED_CHARS)) { throw ModError("Error loading mod \"" + mod.name + "\": Mod name does not follow naming conventions: " "Only characters [a-z0-9_] are allowed."); } scanModIntoMemory(mod.name, mod.path); } // Load and run "mod" scripts for (const ModSpec &mod : m_mods) m_script->loadModFromMemory(mod.name); m_mods_loaded = true; } bool Client::checkBuiltinIntegrity() { // @TODO return true; } void Client::scanModSubfolder(const std::string &mod_name, const std::string &mod_path, std::string mod_subpath) { std::string full_path = mod_path + DIR_DELIM + mod_subpath; std::vector mod = fs::GetDirListing(full_path); for (const fs::DirListNode &j : mod) { std::string filename = j.name; if (j.dir) { scanModSubfolder(mod_name, mod_path, mod_subpath + filename + DIR_DELIM); continue; } std::replace( mod_subpath.begin(), mod_subpath.end(), DIR_DELIM_CHAR, '/'); m_mod_files[mod_name + ":" + mod_subpath + filename] = full_path + filename; } } const std::string &Client::getBuiltinLuaPath() { static const std::string builtin_dir = porting::path_share + DIR_DELIM + "builtin"; return builtin_dir; } const std::string &Client::getClientModsLuaPath() { static const std::string clientmods_dir = porting::path_share + DIR_DELIM + "clientmods"; return clientmods_dir; } const std::vector& Client::getMods() const { static std::vector client_modspec_temp; return client_modspec_temp; } const ModSpec* Client::getModSpec(const std::string &modname) const { return NULL; } void Client::Stop() { m_shutdown = true; // Don't disable this part when modding is disabled, it's used in builtin m_script->on_shutdown(); //request all client managed threads to stop m_mesh_update_thread.stop(); // Save local server map if (m_localdb) { infostream << "Local map saving ended." << std::endl; m_localdb->endSave(); } delete m_script; } bool Client::isShutdown() { return m_shutdown || !m_mesh_update_thread.isRunning(); } Client::~Client() { m_shutdown = true; m_con->Disconnect(); m_mesh_update_thread.stop(); m_mesh_update_thread.wait(); while (!m_mesh_update_thread.m_queue_out.empty()) { MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx(); delete r.mesh; } delete m_inventory_from_server; // Delete detached inventories for (auto &m_detached_inventorie : m_detached_inventories) { delete m_detached_inventorie.second; } // cleanup 3d model meshes on client shutdown while (RenderingEngine::get_mesh_cache()->getMeshCount() != 0) { scene::IAnimatedMesh *mesh = RenderingEngine::get_mesh_cache()->getMeshByIndex(0); if (mesh) RenderingEngine::get_mesh_cache()->removeMesh(mesh); } delete m_minimap; } void Client::connect(Address address, bool is_local_server) { initLocalMapSaving(address, m_address_name, is_local_server); m_con->SetTimeoutMs(0); m_con->Connect(address); } void Client::step(float dtime) { // Limit a bit if (dtime > 2.0) dtime = 2.0; m_animation_time += dtime; if(m_animation_time > 60.0) m_animation_time -= 60.0; m_time_of_day_update_timer += dtime; ReceiveAll(); /* Packet counter */ { float &counter = m_packetcounter_timer; counter -= dtime; if(counter <= 0.0) { counter = 20.0; infostream << "Client packetcounter (" << m_packetcounter_timer << "):"<getName()); } // Not connected, return return; } /* Do stuff if connected */ /* Run Map's timers and unload unused data */ const float map_timer_and_unload_dtime = 5.25; if(m_map_timer_and_unload_interval.step(dtime, map_timer_and_unload_dtime)) { ScopeProfiler sp(g_profiler, "Client: map timer and unload"); std::vector deleted_blocks; m_env.getMap().timerUpdate(map_timer_and_unload_dtime, g_settings->getFloat("client_unload_unused_data_timeout"), g_settings->getS32("client_mapblock_limit"), &deleted_blocks); /* Send info to server NOTE: This loop is intentionally iterated the way it is. */ std::vector::iterator i = deleted_blocks.begin(); std::vector sendlist; for(;;) { if(sendlist.size() == 255 || i == deleted_blocks.end()) { if(sendlist.empty()) break; /* [0] u16 command [2] u8 count [3] v3s16 pos_0 [3+6] v3s16 pos_1 ... */ sendDeletedBlocks(sendlist); if(i == deleted_blocks.end()) break; sendlist.clear(); } sendlist.push_back(*i); ++i; } } /* Send pending messages on out chat queue */ if (!m_out_chat_queue.empty() && canSendChatMessage()) { sendChatMessage(m_out_chat_queue.front()); m_out_chat_queue.pop(); } /* Handle environment */ // Control local player (0ms) LocalPlayer *player = m_env.getLocalPlayer(); assert(player); player->applyControl(dtime, &m_env); // Step environment m_env.step(dtime); m_sound->step(dtime); /* Get events */ while (m_env.hasClientEnvEvents()) { ClientEnvEvent envEvent = m_env.getClientEnvEvent(); if (envEvent.type == CEE_PLAYER_DAMAGE) { u8 damage = envEvent.player_damage.amount; if (envEvent.player_damage.send_to_server) sendDamage(damage); // Add to ClientEvent queue ClientEvent *event = new ClientEvent(); event->type = CE_PLAYER_DAMAGE; event->player_damage.amount = damage; m_client_event_queue.push(event); } } /* Print some info */ float &counter = m_avg_rtt_timer; counter += dtime; if(counter >= 10) { counter = 0.0; // connectedAndInitialized() is true, peer exists. float avg_rtt = getRTT(); infostream << "Client: avg_rtt=" << avg_rtt << std::endl; } /* Send player position to server */ { float &counter = m_playerpos_send_timer; counter += dtime; if((m_state == LC_Ready) && (counter >= m_recommended_send_interval)) { counter = 0.0; sendPlayerPos(); } } /* Replace updated meshes */ { int num_processed_meshes = 0; while (!m_mesh_update_thread.m_queue_out.empty()) { num_processed_meshes++; MinimapMapblock *minimap_mapblock = NULL; bool do_mapper_update = true; MeshUpdateResult r = m_mesh_update_thread.m_queue_out.pop_frontNoEx(); MapBlock *block = m_env.getMap().getBlockNoCreateNoEx(r.p); if (block) { // Delete the old mesh delete block->mesh; block->mesh = nullptr; if (r.mesh) { minimap_mapblock = r.mesh->moveMinimapMapblock(); if (minimap_mapblock == NULL) do_mapper_update = false; bool is_empty = true; for (int l = 0; l < MAX_TILE_LAYERS; l++) if (r.mesh->getMesh(l)->getMeshBufferCount() != 0) is_empty = false; if (is_empty) delete r.mesh; else // Replace with the new mesh block->mesh = r.mesh; } } else { delete r.mesh; } if (m_minimap && do_mapper_update) m_minimap->addBlock(r.p, minimap_mapblock); if (r.ack_block_to_server) { /* Acknowledge block [0] u8 count [1] v3s16 pos_0 */ sendGotBlocks(r.p); } } if (num_processed_meshes > 0) g_profiler->graphAdd("num_processed_meshes", num_processed_meshes); } /* Load fetched media */ if (m_media_downloader && m_media_downloader->isStarted()) { m_media_downloader->step(this); if (m_media_downloader->isDone()) { delete m_media_downloader; m_media_downloader = NULL; } } /* If the server didn't update the inventory in a while, revert the local inventory (so the player notices the lag problem and knows something is wrong). */ if(m_inventory_from_server) { float interval = 10.0; float count_before = floor(m_inventory_from_server_age / interval); m_inventory_from_server_age += dtime; float count_after = floor(m_inventory_from_server_age / interval); if(count_after != count_before) { // Do this every seconds after TOCLIENT_INVENTORY // Reset the locally changed inventory to the authoritative inventory LocalPlayer *player = m_env.getLocalPlayer(); player->inventory = *m_inventory_from_server; m_inventory_updated = true; } } /* Update positions of sounds attached to objects */ { for (auto &m_sounds_to_object : m_sounds_to_objects) { int client_id = m_sounds_to_object.first; u16 object_id = m_sounds_to_object.second; ClientActiveObject *cao = m_env.getActiveObject(object_id); if (!cao) continue; m_sound->updateSoundPosition(client_id, cao->getPosition()); } } /* Handle removed remotely initiated sounds */ m_removed_sounds_check_timer += dtime; if(m_removed_sounds_check_timer >= 2.32) { m_removed_sounds_check_timer = 0; // Find removed sounds and clear references to them std::vector removed_server_ids; for (std::unordered_map::iterator i = m_sounds_server_to_client.begin(); i != m_sounds_server_to_client.end();) { s32 server_id = i->first; int client_id = i->second; ++i; if(!m_sound->soundExists(client_id)) { m_sounds_server_to_client.erase(server_id); m_sounds_client_to_server.erase(client_id); m_sounds_to_objects.erase(client_id); removed_server_ids.push_back(server_id); } } // Sync to server if(!removed_server_ids.empty()) { sendRemovedSounds(removed_server_ids); } } m_mod_storage_save_timer -= dtime; if (m_mod_storage_save_timer <= 0.0f) { verbosestream << "Saving registered mod storages." << std::endl; m_mod_storage_save_timer = g_settings->getFloat("server_map_save_interval"); for (std::unordered_map::const_iterator it = m_mod_storages.begin(); it != m_mod_storages.end(); ++it) { if (it->second->isModified()) { it->second->save(getModStoragePath()); } } } // Write server map if (m_localdb && m_localdb_save_interval.step(dtime, m_cache_save_interval)) { m_localdb->endSave(); m_localdb->beginSave(); } } bool Client::loadMedia(const std::string &data, const std::string &filename) { // Silly irrlicht's const-incorrectness Buffer data_rw(data.c_str(), data.size()); std::string name; const char *image_ext[] = { ".png", ".jpg", ".bmp", ".tga", ".pcx", ".ppm", ".psd", ".wal", ".rgb", NULL }; name = removeStringEnd(filename, image_ext); if (!name.empty()) { verbosestream<<"Client: Attempting to load image " <<"file \""<createMemoryReadFile( *data_rw, data_rw.getSize(), "_tempreadfile"); FATAL_ERROR_IF(!rfile, "Could not create irrlicht memory file."); // Read image video::IImage *img = vdrv->createImageFromFile(rfile); if (!img) { errorstream<<"Client: Cannot create image from data of " <<"file \""<drop(); return false; } m_tsrc->insertSourceImage(filename, img); img->drop(); rfile->drop(); return true; } const char *sound_ext[] = { ".0.ogg", ".1.ogg", ".2.ogg", ".3.ogg", ".4.ogg", ".5.ogg", ".6.ogg", ".7.ogg", ".8.ogg", ".9.ogg", ".ogg", NULL }; name = removeStringEnd(filename, sound_ext); if (!name.empty()) { verbosestream<<"Client: Attempting to load sound " <<"file \""<loadSoundData(name, data); return true; } const char *model_ext[] = { ".x", ".b3d", ".md2", ".obj", NULL }; name = removeStringEnd(filename, model_ext); if (!name.empty()) { verbosestream<<"Client: Storing model into memory: " <<"\""<loadTranslation(data); return true; } errorstream << "Client: Don't know how to load file \"" << filename << "\"" << std::endl; return false; } // Virtual methods from con::PeerHandler void Client::peerAdded(con::Peer *peer) { infostream << "Client::peerAdded(): peer->id=" << peer->id << std::endl; } void Client::deletingPeer(con::Peer *peer, bool timeout) { infostream << "Client::deletingPeer(): " "Server Peer is getting deleted " << "(timeout=" << timeout << ")" << std::endl; if (timeout) { m_access_denied = true; m_access_denied_reason = gettext("Connection timed out."); } } /* u16 command u16 number of files requested for each file { u16 length of name string name } */ void Client::request_media(const std::vector &file_requests) { std::ostringstream os(std::ios_base::binary); writeU16(os, TOSERVER_REQUEST_MEDIA); size_t file_requests_size = file_requests.size(); FATAL_ERROR_IF(file_requests_size > 0xFFFF, "Unsupported number of file requests"); // Packet dynamicly resized NetworkPacket pkt(TOSERVER_REQUEST_MEDIA, 2 + 0); pkt << (u16) (file_requests_size & 0xFFFF); for (const std::string &file_request : file_requests) { pkt << file_request; } Send(&pkt); infostream << "Client: Sending media request list to server (" << file_requests.size() << " files. packet size)" << std::endl; } void Client::initLocalMapSaving(const Address &address, const std::string &hostname, bool is_local_server) { if (!g_settings->getBool("enable_local_map_saving") || is_local_server) { return; } const std::string world_path = porting::path_user + DIR_DELIM + "worlds" + DIR_DELIM + "server_" + hostname + "_" + std::to_string(address.getPort()); fs::CreateAllDirs(world_path); m_localdb = new MapDatabaseSQLite3(world_path); m_localdb->beginSave(); actionstream << "Local map saving started, map will be saved at '" << world_path << "'" << std::endl; } void Client::ReceiveAll() { u64 start_ms = porting::getTimeMs(); for(;;) { // Limit time even if there would be huge amounts of data to // process if(porting::getTimeMs() > start_ms + 100) break; try { Receive(); g_profiler->graphAdd("client_received_packets", 1); } catch(con::NoIncomingDataException &e) { break; } catch(con::InvalidIncomingDataException &e) { infostream<<"Client::ReceiveAll(): " "InvalidIncomingDataException: what()=" <Receive(&pkt); ProcessData(&pkt); } inline void Client::handleCommand(NetworkPacket* pkt) { const ToClientCommandHandler& opHandle = toClientCommandTable[pkt->getCommand()]; (this->*opHandle.handler)(pkt); } /* sender_peer_id given to this shall be quaranteed to be a valid peer */ void Client::ProcessData(NetworkPacket *pkt) { ToClientCommand command = (ToClientCommand) pkt->getCommand(); u32 sender_peer_id = pkt->getPeerId(); //infostream<<"Client: received command="< &blocks) { NetworkPacket pkt(TOSERVER_DELETEDBLOCKS, 1 + sizeof(v3s16) * blocks.size()); pkt << (u8) blocks.size(); for (const v3s16 &block : blocks) { pkt << block; } Send(&pkt); } void Client::sendGotBlocks(v3s16 block) { NetworkPacket pkt(TOSERVER_GOTBLOCKS, 1 + 6); pkt << (u8) 1 << block; Send(&pkt); } void Client::sendRemovedSounds(std::vector &soundList) { size_t server_ids = soundList.size(); assert(server_ids <= 0xFFFF); NetworkPacket pkt(TOSERVER_REMOVED_SOUNDS, 2 + server_ids * 4); pkt << (u16) (server_ids & 0xFFFF); for (int sound_id : soundList) pkt << sound_id; Send(&pkt); } void Client::sendNodemetaFields(v3s16 p, const std::string &formname, const StringMap &fields) { size_t fields_size = fields.size(); FATAL_ERROR_IF(fields_size > 0xFFFF, "Unsupported number of nodemeta fields"); NetworkPacket pkt(TOSERVER_NODEMETA_FIELDS, 0); pkt << p << formname << (u16) (fields_size & 0xFFFF); StringMap::const_iterator it; for (it = fields.begin(); it != fields.end(); ++it) { const std::string &name = it->first; const std::string &value = it->second; pkt << name; pkt.putLongString(value); } Send(&pkt); } void Client::sendInventoryFields(const std::string &formname, const StringMap &fields) { size_t fields_size = fields.size(); FATAL_ERROR_IF(fields_size > 0xFFFF, "Unsupported number of inventory fields"); NetworkPacket pkt(TOSERVER_INVENTORY_FIELDS, 0); pkt << formname << (u16) (fields_size & 0xFFFF); StringMap::const_iterator it; for (it = fields.begin(); it != fields.end(); ++it) { const std::string &name = it->first; const std::string &value = it->second; pkt << name; pkt.putLongString(value); } Send(&pkt); } void Client::sendInventoryAction(InventoryAction *a) { std::ostringstream os(std::ios_base::binary); a->serialize(os); // Make data buffer std::string s = os.str(); NetworkPacket pkt(TOSERVER_INVENTORY_ACTION, s.size()); pkt.putRawString(s.c_str(),s.size()); Send(&pkt); } bool Client::canSendChatMessage() const { u32 now = time(NULL); float time_passed = now - m_last_chat_message_sent; float virt_chat_message_allowance = m_chat_message_allowance + time_passed * (CLIENT_CHAT_MESSAGE_LIMIT_PER_10S / 8.0f); if (virt_chat_message_allowance < 1.0f) return false; return true; } void Client::sendChatMessage(const std::wstring &message) { const s16 max_queue_size = g_settings->getS16("max_out_chat_queue_size"); if (canSendChatMessage()) { u32 now = time(NULL); float time_passed = now - m_last_chat_message_sent; m_last_chat_message_sent = time(NULL); m_chat_message_allowance += time_passed * (CLIENT_CHAT_MESSAGE_LIMIT_PER_10S / 8.0f); if (m_chat_message_allowance > CLIENT_CHAT_MESSAGE_LIMIT_PER_10S) m_chat_message_allowance = CLIENT_CHAT_MESSAGE_LIMIT_PER_10S; m_chat_message_allowance -= 1.0f; NetworkPacket pkt(TOSERVER_CHAT_MESSAGE, 2 + message.size() * sizeof(u16)); pkt << message; Send(&pkt); } else if (m_out_chat_queue.size() < (u16) max_queue_size || max_queue_size == -1) { m_out_chat_queue.push(message); } else { infostream << "Could not queue chat message because maximum out chat queue size (" << max_queue_size << ") is reached." << std::endl; } } void Client::clearOutChatQueue() { m_out_chat_queue = std::queue(); } void Client::sendChangePassword(const std::string &oldpassword, const std::string &newpassword) { LocalPlayer *player = m_env.getLocalPlayer(); if (player == NULL) return; // get into sudo mode and then send new password to server m_password = oldpassword; m_new_password = newpassword; startAuth(choseAuthMech(m_sudo_auth_methods)); } void Client::sendDamage(u8 damage) { NetworkPacket pkt(TOSERVER_DAMAGE, sizeof(u8)); pkt << damage; Send(&pkt); } void Client::sendRespawn() { NetworkPacket pkt(TOSERVER_RESPAWN, 0); Send(&pkt); } void Client::sendReady() { NetworkPacket pkt(TOSERVER_CLIENT_READY, 1 + 1 + 1 + 1 + 2 + sizeof(char) * strlen(g_version_hash)); pkt << (u8) VERSION_MAJOR << (u8) VERSION_MINOR << (u8) VERSION_PATCH << (u8) 0 << (u16) strlen(g_version_hash); pkt.putRawString(g_version_hash, (u16) strlen(g_version_hash)); Send(&pkt); } void Client::sendPlayerPos() { LocalPlayer *myplayer = m_env.getLocalPlayer(); if (!myplayer) return; ClientMap &map = m_env.getClientMap(); u8 camera_fov = map.getCameraFov(); u8 wanted_range = map.getControl().wanted_range; // Save bandwidth by only updating position when something changed if(myplayer->last_position == myplayer->getPosition() && myplayer->last_speed == myplayer->getSpeed() && myplayer->last_pitch == myplayer->getPitch() && myplayer->last_yaw == myplayer->getYaw() && myplayer->last_keyPressed == myplayer->keyPressed && myplayer->last_camera_fov == camera_fov && myplayer->last_wanted_range == wanted_range) return; myplayer->last_position = myplayer->getPosition(); myplayer->last_speed = myplayer->getSpeed(); myplayer->last_pitch = myplayer->getPitch(); myplayer->last_yaw = myplayer->getYaw(); myplayer->last_keyPressed = myplayer->keyPressed; myplayer->last_camera_fov = camera_fov; myplayer->last_wanted_range = wanted_range; NetworkPacket pkt(TOSERVER_PLAYERPOS, 12 + 12 + 4 + 4 + 4 + 1 + 1); writePlayerPos(myplayer, &map, &pkt); Send(&pkt); } void Client::sendPlayerItem(u16 item) { LocalPlayer *myplayer = m_env.getLocalPlayer(); if (!myplayer) return; NetworkPacket pkt(TOSERVER_PLAYERITEM, 2); pkt << item; Send(&pkt); } void Client::removeNode(v3s16 p) { std::map modified_blocks; try { m_env.getMap().removeNodeAndUpdate(p, modified_blocks); } catch(InvalidPositionException &e) { } for (const auto &modified_block : modified_blocks) { addUpdateMeshTaskWithEdge(modified_block.first, false, true); } } /** * Helper function for Client Side Modding * Flavour is applied there, this should not be used for core engine * @param p * @param is_valid_position * @return */ MapNode Client::getNode(v3s16 p, bool *is_valid_position) { if (checkCSMFlavourLimit(CSMFlavourLimit::CSM_FL_LOOKUP_NODES)) { v3s16 ppos = floatToInt(m_env.getLocalPlayer()->getPosition(), BS); if ((u32) ppos.getDistanceFrom(p) > m_csm_noderange_limit) { *is_valid_position = false; return {}; } } return m_env.getMap().getNodeNoEx(p, is_valid_position); } void Client::addNode(v3s16 p, MapNode n, bool remove_metadata) { //TimeTaker timer1("Client::addNode()"); std::map modified_blocks; try { //TimeTaker timer3("Client::addNode(): addNodeAndUpdate"); m_env.getMap().addNodeAndUpdate(p, n, modified_blocks, remove_metadata); } catch(InvalidPositionException &e) { } for (const auto &modified_block : modified_blocks) { addUpdateMeshTaskWithEdge(modified_block.first, false, true); } } void Client::setPlayerControl(PlayerControl &control) { LocalPlayer *player = m_env.getLocalPlayer(); assert(player); player->control = control; } void Client::selectPlayerItem(u16 item) { m_playeritem = item; m_inventory_updated = true; sendPlayerItem(item); } // Returns true if the inventory of the local player has been // updated from the server. If it is true, it is set to false. bool Client::getLocalInventoryUpdated() { bool updated = m_inventory_updated; m_inventory_updated = false; return updated; } // Copies the inventory of the local player to parameter void Client::getLocalInventory(Inventory &dst) { LocalPlayer *player = m_env.getLocalPlayer(); assert(player); dst = player->inventory; } Inventory* Client::getInventory(const InventoryLocation &loc) { switch(loc.type){ case InventoryLocation::UNDEFINED: {} break; case InventoryLocation::CURRENT_PLAYER: { LocalPlayer *player = m_env.getLocalPlayer(); assert(player); return &player->inventory; } break; case InventoryLocation::PLAYER: { // Check if we are working with local player inventory LocalPlayer *player = m_env.getLocalPlayer(); if (!player || strcmp(player->getName(), loc.name.c_str()) != 0) return NULL; return &player->inventory; } break; case InventoryLocation::NODEMETA: { NodeMetadata *meta = m_env.getMap().getNodeMetadata(loc.p); if(!meta) return NULL; return meta->getInventory(); } break; case InventoryLocation::DETACHED: { if (m_detached_inventories.count(loc.name) == 0) return NULL; return m_detached_inventories[loc.name]; } break; default: FATAL_ERROR("Invalid inventory location type."); break; } return NULL; } void Client::inventoryAction(InventoryAction *a) { /* Send it to the server */ sendInventoryAction(a); /* Predict some local inventory changes */ a->clientApply(this, this); // Remove it delete a; } float Client::getAnimationTime() { return m_animation_time; } int Client::getCrackLevel() { return m_crack_level; } v3s16 Client::getCrackPos() { return m_crack_pos; } void Client::setCrack(int level, v3s16 pos) { int old_crack_level = m_crack_level; v3s16 old_crack_pos = m_crack_pos; m_crack_level = level; m_crack_pos = pos; if(old_crack_level >= 0 && (level < 0 || pos != old_crack_pos)) { // remove old crack addUpdateMeshTaskForNode(old_crack_pos, false, true); } if(level >= 0 && (old_crack_level < 0 || pos != old_crack_pos)) { // add new crack addUpdateMeshTaskForNode(pos, false, true); } } u16 Client::getHP() { LocalPlayer *player = m_env.getLocalPlayer(); assert(player); return player->hp; } bool Client::getChatMessage(std::wstring &res) { if (m_chat_queue.empty()) return false; ChatMessage *chatMessage = m_chat_queue.front(); m_chat_queue.pop(); res = L""; switch (chatMessage->type) { case CHATMESSAGE_TYPE_RAW: case CHATMESSAGE_TYPE_ANNOUNCE: case CHATMESSAGE_TYPE_SYSTEM: res = chatMessage->message; break; case CHATMESSAGE_TYPE_NORMAL: { if (!chatMessage->sender.empty()) res = L"<" + chatMessage->sender + L"> " + chatMessage->message; else res = chatMessage->message; break; } default: break; } delete chatMessage; return true; } void Client::typeChatMessage(const std::wstring &message) { // Discard empty line if (message.empty()) return; // If message was ate by script API, don't send it to server if (m_script->on_sending_message(wide_to_utf8(message))) { return; } // Send to others sendChatMessage(message); // Show locally if (message[0] != L'/') { // compatibility code if (m_proto_ver < 29) { LocalPlayer *player = m_env.getLocalPlayer(); assert(player); std::wstring name = narrow_to_wide(player->getName()); pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_NORMAL, message, name)); } } } void Client::addUpdateMeshTask(v3s16 p, bool ack_to_server, bool urgent) { // Check if the block exists to begin with. In the case when a non-existing // neighbor is automatically added, it may not. In that case we don't want // to tell the mesh update thread about it. MapBlock *b = m_env.getMap().getBlockNoCreateNoEx(p); if (b == NULL) return; m_mesh_update_thread.updateBlock(&m_env.getMap(), p, ack_to_server, urgent); } void Client::addUpdateMeshTaskWithEdge(v3s16 blockpos, bool ack_to_server, bool urgent) { try{ addUpdateMeshTask(blockpos, ack_to_server, urgent); } catch(InvalidPositionException &e){} // Leading edge for (int i=0;i<6;i++) { try{ v3s16 p = blockpos + g_6dirs[i]; addUpdateMeshTask(p, false, urgent); } catch(InvalidPositionException &e){} } } void Client::addUpdateMeshTaskForNode(v3s16 nodepos, bool ack_to_server, bool urgent) { { v3s16 p = nodepos; infostream<<"Client::addUpdateMeshTaskForNode(): " <<"("<Connected(); } const Address Client::getServerAddress() { return m_con->GetPeerAddress(PEER_ID_SERVER); } float Client::mediaReceiveProgress() { if (m_media_downloader) return m_media_downloader->getProgress(); return 1.0; // downloader only exists when not yet done } typedef struct TextureUpdateArgs { gui::IGUIEnvironment *guienv; u64 last_time_ms; u16 last_percent; const wchar_t* text_base; ITextureSource *tsrc; } TextureUpdateArgs; void texture_update_progress(void *args, u32 progress, u32 max_progress) { TextureUpdateArgs* targs = (TextureUpdateArgs*) args; u16 cur_percent = ceil(progress / (double) max_progress * 100.); // update the loading menu -- if neccessary bool do_draw = false; u64 time_ms = targs->last_time_ms; if (cur_percent != targs->last_percent) { targs->last_percent = cur_percent; time_ms = porting::getTimeMs(); // only draw when the user will notice something: do_draw = (time_ms - targs->last_time_ms > 100); } if (do_draw) { targs->last_time_ms = time_ms; std::basic_stringstream strm; strm << targs->text_base << " " << targs->last_percent << "%..."; RenderingEngine::draw_load_screen(strm.str(), targs->guienv, targs->tsrc, 0, 72 + (u16) ((18. / 100.) * (double) targs->last_percent), true); } } void Client::afterContentReceived() { infostream<<"Client::afterContentReceived() started"<rebuildImagesAndTextures(); delete[] text; // Rebuild shaders infostream<<"- Rebuilding shaders"<rebuildShaders(); delete[] text; // Update node aliases infostream<<"- Updating node aliases"<updateAliases(m_itemdef); for (const auto &path : getTextureDirs()) m_nodedef->applyTextureOverrides(path + DIR_DELIM + "override.txt"); m_nodedef->setNodeRegistrationStatus(true); m_nodedef->runNodeResolveCallbacks(); delete[] text; // Update node textures and assign shaders to each tile infostream<<"- Updating node textures"<updateTextures(this, texture_update_progress, &tu_args); delete[] tu_args.text_base; // Start mesh update thread after setting up content definitions infostream<<"- Starting mesh update thread"<getBool("enable_client_modding")) { m_script->on_client_ready(m_env.getLocalPlayer()); } text = wgettext("Done!"); RenderingEngine::draw_load_screen(text, guienv, m_tsrc, 0, 100); infostream<<"Client::afterContentReceived() done"<getPeerStat(PEER_ID_SERVER,con::AVG_RTT); } float Client::getCurRate() { return (m_con->getLocalStat(con::CUR_INC_RATE) + m_con->getLocalStat(con::CUR_DL_RATE)); } void Client::makeScreenshot() { irr::video::IVideoDriver *driver = RenderingEngine::get_video_driver(); irr::video::IImage* const raw_image = driver->createScreenShot(); if (!raw_image) return; time_t t = time(NULL); struct tm *tm = localtime(&t); char timetstamp_c[64]; strftime(timetstamp_c, sizeof(timetstamp_c), "%Y%m%d_%H%M%S", tm); std::string filename_base = g_settings->get("screenshot_path") + DIR_DELIM + std::string("screenshot_") + std::string(timetstamp_c); std::string filename_ext = "." + g_settings->get("screenshot_format"); std::string filename; u32 quality = (u32)g_settings->getS32("screenshot_quality"); quality = MYMIN(MYMAX(quality, 0), 100) / 100.0 * 255; // Try to find a unique filename unsigned serial = 0; while (serial < SCREENSHOT_MAX_SERIAL_TRIES) { filename = filename_base + (serial > 0 ? ("_" + itos(serial)) : "") + filename_ext; std::ifstream tmp(filename.c_str()); if (!tmp.good()) break; // File did not apparently exist, we'll go with it serial++; } if (serial == SCREENSHOT_MAX_SERIAL_TRIES) { infostream << "Could not find suitable filename for screenshot" << std::endl; } else { irr::video::IImage* const image = driver->createImage(video::ECF_R8G8B8, raw_image->getDimension()); if (image) { raw_image->copyTo(image); std::ostringstream sstr; if (driver->writeImageToFile(image, filename.c_str(), quality)) { sstr << "Saved screenshot to '" << filename << "'"; } else { sstr << "Failed to save screenshot '" << filename << "'"; } pushToChatQueue(new ChatMessage(CHATMESSAGE_TYPE_SYSTEM, narrow_to_wide(sstr.str()))); infostream << sstr.str() << std::endl; image->drop(); } } raw_image->drop(); } bool Client::shouldShowMinimap() const { return !m_minimap_disabled_by_server; } void Client::pushToEventQueue(ClientEvent *event) { m_client_event_queue.push(event); } void Client::showMinimap(const bool show) { m_game_ui->showMinimap(show); } // IGameDef interface // Under envlock IItemDefManager* Client::getItemDefManager() { return m_itemdef; } const NodeDefManager* Client::getNodeDefManager() { return m_nodedef; } ICraftDefManager* Client::getCraftDefManager() { return NULL; //return m_craftdef; } ITextureSource* Client::getTextureSource() { return m_tsrc; } IShaderSource* Client::getShaderSource() { return m_shsrc; } u16 Client::allocateUnknownNodeId(const std::string &name) { errorstream << "Client::allocateUnknownNodeId(): " << "Client cannot allocate node IDs" << std::endl; FATAL_ERROR("Client allocated unknown node"); return CONTENT_IGNORE; } ISoundManager* Client::getSoundManager() { return m_sound; } MtEventManager* Client::getEventManager() { return m_event; } ParticleManager* Client::getParticleManager() { return &m_particle_manager; } scene::IAnimatedMesh* Client::getMesh(const std::string &filename, bool cache) { StringMap::const_iterator it = m_mesh_data.find(filename); if (it == m_mesh_data.end()) { errorstream << "Client::getMesh(): Mesh not found: \"" << filename << "\"" << std::endl; return NULL; } const std::string &data = it->second; // Create the mesh, remove it from cache and return it // This allows unique vertex colors and other properties for each instance Buffer data_rw(data.c_str(), data.size()); // Const-incorrect Irrlicht io::IReadFile *rfile = RenderingEngine::get_filesystem()->createMemoryReadFile( *data_rw, data_rw.getSize(), filename.c_str()); FATAL_ERROR_IF(!rfile, "Could not create/open RAM file"); scene::IAnimatedMesh *mesh = RenderingEngine::get_scene_manager()->getMesh(rfile); rfile->drop(); mesh->grab(); if (!cache) RenderingEngine::get_mesh_cache()->removeMesh(mesh); return mesh; } const std::string* Client::getModFile(const std::string &filename) { StringMap::const_iterator it = m_mod_files.find(filename); if (it == m_mod_files.end()) { errorstream << "Client::getModFile(): File not found: \"" << filename << "\"" << std::endl; return NULL; } return &it->second; } bool Client::registerModStorage(ModMetadata *storage) { if (m_mod_storages.find(storage->getModName()) != m_mod_storages.end()) { errorstream << "Unable to register same mod storage twice. Storage name: " << storage->getModName() << std::endl; return false; } m_mod_storages[storage->getModName()] = storage; return true; } void Client::unregisterModStorage(const std::string &name) { std::unordered_map::const_iterator it = m_mod_storages.find(name); if (it != m_mod_storages.end()) { // Save unconditionaly on unregistration it->second->save(getModStoragePath()); m_mod_storages.erase(name); } } std::string Client::getModStoragePath() const { return porting::path_user + DIR_DELIM + "client" + DIR_DELIM + "mod_storage"; } /* * Mod channels */ bool Client::joinModChannel(const std::string &channel) { if (m_modchannel_mgr->channelRegistered(channel)) return false; NetworkPacket pkt(TOSERVER_MODCHANNEL_JOIN, 2 + channel.size()); pkt << channel; Send(&pkt); m_modchannel_mgr->joinChannel(channel, 0); return true; } bool Client::leaveModChannel(const std::string &channel) { if (!m_modchannel_mgr->channelRegistered(channel)) return false; NetworkPacket pkt(TOSERVER_MODCHANNEL_LEAVE, 2 + channel.size()); pkt << channel; Send(&pkt); m_modchannel_mgr->leaveChannel(channel, 0); return true; } bool Client::sendModChannelMessage(const std::string &channel, const std::string &message) { if (!m_modchannel_mgr->canWriteOnChannel(channel)) return false; if (message.size() > STRING_MAX_LEN) { warningstream << "ModChannel message too long, dropping before sending " << " (" << message.size() << " > " << STRING_MAX_LEN << ", channel: " << channel << ")" << std::endl; return false; } // @TODO: do some client rate limiting NetworkPacket pkt(TOSERVER_MODCHANNEL_MSG, 2 + channel.size() + 2 + message.size()); pkt << channel << message; Send(&pkt); return true; } ModChannel* Client::getModChannel(const std::string &channel) { return m_modchannel_mgr->getModChannel(channel); }