]> git.lizzy.rs Git - minetest.git/blobdiff - src/server.cpp
Disable Prometheus in singleplayer mode
[minetest.git] / src / server.cpp
index f1613cffe4bc11f1d564b1e42b594f3bc0da2a56..bced71afa3b67998af6bf81ce3092046bc8633b4 100644 (file)
@@ -34,8 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "version.h"
 #include "filesys.h"
 #include "mapblock.h"
-#include "serverobject.h"
-#include "genericobject.h"
+#include "server/serveractiveobject.h"
 #include "settings.h"
 #include "profiler.h"
 #include "log.h"
@@ -48,7 +47,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "mapgen/mg_biome.h"
 #include "content_mapnode.h"
 #include "content_nodemeta.h"
-#include "content_sao.h"
 #include "content/mods.h"
 #include "modchannels.h"
 #include "serverlist.h"
@@ -65,6 +63,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "chatmessage.h"
 #include "chat_interface.h"
 #include "remoteplayer.h"
+#include "server/player_sao.h"
+#include "server/serverinventorymgr.h"
+#include "translation.h"
+#include "database/database-sqlite3.h"
+#if USE_POSTGRESQL
+#include "database/database-postgresql.h"
+#endif
+#include "database/database-files.h"
+#include "database/database-dummy.h"
+#include "gameparams.h"
 
 class ClientNotFoundException : public BaseException
 {
@@ -102,7 +110,13 @@ void *ServerThread::run()
         * doesn't busy wait) and will process any remaining packets.
         */
 
-       m_server->AsyncRunStep(true);
+       try {
+               m_server->AsyncRunStep(true);
+       } catch (con::ConnectionBindFailed &e) {
+               m_server->setAsyncFatalError(e.what());
+       } catch (LuaError &e) {
+               m_server->setAsyncFatalError(e);
+       }
 
        while (!stopRequested()) {
                try {
@@ -116,8 +130,7 @@ void *ServerThread::run()
                } catch (con::ConnectionBindFailed &e) {
                        m_server->setAsyncFatalError(e.what());
                } catch (LuaError &e) {
-                       m_server->setAsyncFatalError(
-                                       "ServerThread::run Lua: " + std::string(e.what()));
+                       m_server->setAsyncFatalError(e);
                }
        }
 
@@ -126,24 +139,31 @@ void *ServerThread::run()
        return nullptr;
 }
 
-v3f ServerSoundParams::getPos(ServerEnvironment *env, bool *pos_exists) const
+v3f ServerPlayingSound::getPos(ServerEnvironment *env, bool *pos_exists) const
 {
-       if(pos_exists) *pos_exists = false;
-       switch(type){
-       case SSP_LOCAL:
+       if (pos_exists)
+               *pos_exists = false;
+
+       switch (type ){
+       case SoundLocation::Local:
                return v3f(0,0,0);
-       case SSP_POSITIONAL:
-               if(pos_exists) *pos_exists = true;
+       case SoundLocation::Position:
+               if (pos_exists)
+                       *pos_exists = true;
                return pos;
-       case SSP_OBJECT: {
-               if(object == 0)
-                       return v3f(0,0,0);
-               ServerActiveObject *sao = env->getActiveObject(object);
-               if(!sao)
-                       return v3f(0,0,0);
-               if(pos_exists) *pos_exists = true;
-               return sao->getBasePosition(); }
+       case SoundLocation::Object:
+               {
+                       if (object == 0)
+                               return v3f(0,0,0);
+                       ServerActiveObject *sao = env->getActiveObject(object);
+                       if (!sao)
+                               return v3f(0,0,0);
+                       if (pos_exists)
+                               *pos_exists = true;
+                       return sao->getBasePosition();
+               }
        }
+
        return v3f(0,0,0);
 }
 
@@ -212,7 +232,8 @@ Server::Server(
                bool simple_singleplayer_mode,
                Address bind_addr,
                bool dedicated,
-               ChatInterface *iface
+               ChatInterface *iface,
+               std::string *on_shutdown_errmsg
        ):
        m_bind_addr(bind_addr),
        m_path_world(path_world),
@@ -229,18 +250,60 @@ Server::Server(
        m_nodedef(createNodeDefManager()),
        m_craftdef(createCraftDefManager()),
        m_thread(new ServerThread(this)),
-       m_uptime(0),
        m_clients(m_con),
        m_admin_chat(iface),
+       m_on_shutdown_errmsg(on_shutdown_errmsg),
        m_modchannel_mgr(new ModChannelMgr())
 {
-       m_lag = g_settings->getFloat("dedicated_server_step");
-
        if (m_path_world.empty())
                throw ServerError("Supplied empty world path");
 
        if (!gamespec.isValid())
                throw ServerError("Supplied invalid gamespec");
+
+#if USE_PROMETHEUS
+       if (!simple_singleplayer_mode)
+               m_metrics_backend = std::unique_ptr<MetricsBackend>(createPrometheusMetricsBackend());
+       else
+#else
+       if (true)
+#endif
+               m_metrics_backend = std::make_unique<MetricsBackend>();
+
+       m_uptime_counter = m_metrics_backend->addCounter("minetest_core_server_uptime", "Server uptime (in seconds)");
+       m_player_gauge = m_metrics_backend->addGauge("minetest_core_player_number", "Number of connected players");
+
+       m_timeofday_gauge = m_metrics_backend->addGauge(
+                       "minetest_core_timeofday",
+                       "Time of day value");
+
+       m_lag_gauge = m_metrics_backend->addGauge(
+                       "minetest_core_latency",
+                       "Latency value (in seconds)");
+
+
+       const std::string aom_types[] = {"reliable", "unreliable"};
+       for (u32 i = 0; i < ARRLEN(aom_types); i++) {
+               std::string help_str("Number of active object messages generated (");
+               help_str.append(aom_types[i]).append(")");
+               m_aom_buffer_counter[i] = m_metrics_backend->addCounter(
+                               "minetest_core_aom_generated_count", help_str,
+                               {{"type", aom_types[i]}});
+       }
+
+       m_packet_recv_counter = m_metrics_backend->addCounter(
+                       "minetest_core_server_packet_recv",
+                       "Processable packets received");
+
+       m_packet_recv_processed_counter = m_metrics_backend->addCounter(
+                       "minetest_core_server_packet_recv_processed",
+                       "Valid received packets processed");
+
+       m_map_edit_event_counter = m_metrics_backend->addCounter(
+                       "minetest_core_map_edit_events",
+                       "Number of map edit events");
+
+       m_lag_gauge->set(g_settings->getFloat("dedicated_server_step"));
 }
 
 Server::~Server()
@@ -285,7 +348,18 @@ Server::~Server()
 
                // Execute script shutdown hooks
                infostream << "Executing shutdown hooks" << std::endl;
-               m_script->on_shutdown();
+               try {
+                       m_script->on_shutdown();
+               } catch (ModError &e) {
+                       errorstream << "ModError: " << e.what() << std::endl;
+                       if (m_on_shutdown_errmsg) {
+                               if (m_on_shutdown_errmsg->empty()) {
+                                       *m_on_shutdown_errmsg = std::string("ModError: ") + e.what();
+                               } else {
+                                       *m_on_shutdown_errmsg += std::string("\nModError: ") + e.what();
+                               }
+                       }
+               }
 
                infostream << "Server: Saving environment metadata" << std::endl;
                m_env->saveMeta();
@@ -297,10 +371,15 @@ Server::~Server()
                delete m_thread;
        }
 
+       // Write any changes before deletion.
+       if (m_mod_storage_database)
+               m_mod_storage_database->endSave();
+
        // Delete things in the reverse order of creation
        delete m_emerge;
        delete m_env;
        delete m_rollback;
+       delete m_mod_storage_database;
        delete m_banmanager;
        delete m_itemdef;
        delete m_nodedef;
@@ -309,11 +388,8 @@ Server::~Server()
        // Deinitialize scripting
        infostream << "Server: Deinitializing scripting" << std::endl;
        delete m_script;
-
-       // Delete detached inventories
-       for (auto &detached_inventory : m_detached_inventories) {
-               delete detached_inventory.second;
-       }
+       delete m_startup_server_map; // if available
+       delete m_game_settings;
 
        while (!m_unsent_map_edit_queue.empty()) {
                delete m_unsent_map_edit_queue.front();
@@ -331,37 +407,55 @@ void Server::init()
        infostream << "- world:  " << m_path_world << std::endl;
        infostream << "- game:   " << m_gamespec.path << std::endl;
 
+       m_game_settings = Settings::createLayer(SL_GAME);
+
        // Create world if it doesn't exist
-       if (!loadGameConfAndInitWorld(m_path_world, m_gamespec))
-               throw ServerError("Failed to initialize world");
+       try {
+               loadGameConfAndInitWorld(m_path_world,
+                               fs::GetFilenameFromPath(m_path_world.c_str()),
+                               m_gamespec, false);
+       } catch (const BaseException &e) {
+               throw ServerError(std::string("Failed to initialize world: ") + e.what());
+       }
 
        // Create emerge manager
-       m_emerge = new EmergeManager(this);
+       m_emerge = new EmergeManager(this, m_metrics_backend.get());
 
        // Create ban manager
        std::string ban_path = m_path_world + DIR_DELIM "ipban.txt";
        m_banmanager = new BanManager(ban_path);
 
-       m_modmgr = std::unique_ptr<ServerModManager>(new ServerModManager(m_path_world));
-       std::vector<ModSpec> unsatisfied_mods = m_modmgr->getUnsatisfiedMods();
+       // Create mod storage database and begin a save for later
+       m_mod_storage_database = openModStorageDatabase(m_path_world);
+       m_mod_storage_database->beginSave();
+
+       m_modmgr = std::make_unique<ServerModManager>(m_path_world);
+
        // complain about mods with unsatisfied dependencies
        if (!m_modmgr->isConsistent()) {
-               m_modmgr->printUnsatisfiedModsError();
+               std::string error = m_modmgr->getUnsatisfiedModsError();
+               throw ServerError(error);
        }
 
        //lock environment
        MutexAutoLock envlock(m_env_mutex);
 
        // Create the Map (loads map_meta.txt, overriding configured mapgen params)
-       ServerMap *servermap = new ServerMap(m_path_world, this, m_emerge);
+       ServerMap *servermap = new ServerMap(m_path_world, this, m_emerge, m_metrics_backend.get());
+       m_startup_server_map = servermap;
 
        // Initialize scripting
        infostream << "Server: Initializing Lua" << std::endl;
 
        m_script = new ServerScripting(this);
 
+       // Must be created before mod loading because we have some inventory creation
+       m_inventory_mgr = std::make_unique<ServerInventoryManager>();
+
        m_script->loadMod(getBuiltinLuaPath() + DIR_DELIM "init.lua", BUILTIN_MOD_NAME);
+       m_script->checkSetByBuiltin();
 
+       m_gamespec.checkAndLog();
        m_modmgr->loadMods(m_script);
 
        // Read Textures and calculate sha1 sums
@@ -374,23 +468,30 @@ void Server::init()
        std::vector<std::string> paths;
        fs::GetRecursiveDirs(paths, g_settings->get("texture_path"));
        fs::GetRecursiveDirs(paths, m_gamespec.path + DIR_DELIM + "textures");
-       for (const std::string &path : paths)
-               m_nodedef->applyTextureOverrides(path + DIR_DELIM + "override.txt");
+       for (const std::string &path : paths) {
+               TextureOverrideSource override_source(path + DIR_DELIM + "override.txt");
+               m_nodedef->applyTextureOverrides(override_source.getNodeTileOverrides());
+               m_itemdef->applyTextureOverrides(override_source.getItemTextureOverrides());
+       }
 
        m_nodedef->setNodeRegistrationStatus(true);
 
        // Perform pending node name resolutions
        m_nodedef->runNodeResolveCallbacks();
 
-       // unmap node names for connected nodeboxes
-       m_nodedef->mapNodeboxConnections();
+       // unmap node names in cross-references
+       m_nodedef->resolveCrossrefs();
 
        // init the recipe hashes to speed up crafting
        m_craftdef->initHashes(this);
 
        // Initialize Environment
-       m_env = new ServerEnvironment(servermap, m_script, this, m_path_world);
+       m_startup_server_map = nullptr; // Ownership moved to ServerEnvironment
+       m_env = new ServerEnvironment(servermap, m_script, this,
+               m_path_world, m_metrics_backend.get());
+       m_env->init();
 
+       m_inventory_mgr->setEnv(m_env);
        m_clients.setEnv(m_env);
 
        if (!servermap->settings_mgr.makeMapgenParams())
@@ -407,11 +508,16 @@ void Server::init()
        // Give environment reference to scripting api
        m_script->initializeEnvironment(m_env);
 
+       // Do this after regular script init is done
+       m_script->initAsync();
+
        // Register us to receive map edit events
        servermap->addEventReceiver(this);
 
        m_env->loadMeta();
 
+       // Those settings can be overwritten in world.mt, they are
+       // intended to be cached after environment loading.
        m_liquid_transform_every = g_settings->getFloat("liquid_update");
        m_max_chatmessage_length = g_settings->getU16("chat_message_max_size");
        m_csm_restriction_flags = g_settings->getU64("csm_restriction_flags");
@@ -420,6 +526,8 @@ void Server::init()
 
 void Server::start()
 {
+       init();
+
        infostream << "Starting server on " << m_bind_addr.serializeString()
                        << "..." << std::endl;
 
@@ -435,16 +543,17 @@ void Server::start()
 
        // ASCII art for the win!
        std::cerr
-               << "        .__               __                   __   " << std::endl
-               << "  _____ |__| ____   _____/  |_  ____   _______/  |_ " << std::endl
-               << " /     \\|  |/    \\_/ __ \\   __\\/ __ \\ /  ___/\\   __\\" << std::endl
-               << "|  Y Y  \\  |   |  \\  ___/|  | \\  ___/ \\___ \\  |  |  " << std::endl
-               << "|__|_|  /__|___|  /\\___  >__|  \\___  >____  > |__|  " << std::endl
-               << "      \\/        \\/     \\/          \\/     \\/        " << std::endl;
+               << "         __.               __.                 __.  " << std::endl
+               << "  _____ |__| ____   _____ /  |_  _____  _____ /  |_ " << std::endl
+               << " /     \\|  |/    \\ /  __ \\    _\\/  __ \\/   __>    _\\" << std::endl
+               << "|  Y Y  \\  |   |  \\   ___/|  | |   ___/\\___  \\|  |  " << std::endl
+               << "|__|_|  /  |___|  /\\______>  |  \\______>_____/|  |  " << std::endl
+               << "      \\/ \\/     \\/         \\/                  \\/   " << std::endl;
        actionstream << "World at [" << m_path_world << "]" << std::endl;
        actionstream << "Server for gameid=\"" << m_gamespec.id
-                       << "\" listening on " << m_bind_addr.serializeString() << ":"
-                       << m_bind_addr.getPort() << "." << std::endl;
+                       << "\" listening on ";
+       m_bind_addr.print(actionstream);
+       actionstream << "." << std::endl;
 }
 
 void Server::stop()
@@ -453,9 +562,7 @@ void Server::stop()
 
        // Stop threads (set run=false first so both start stopping)
        m_thread->stop();
-       //m_emergethread.setRun(false);
        m_thread->wait();
-       //m_emergethread.stop();
 
        infostream<<"Server: Threads stopped"<<std::endl;
 }
@@ -508,9 +615,7 @@ void Server::AsyncRunStep(bool initial_step)
        /*
                Update uptime
        */
-       {
-               m_uptime.set(m_uptime.get() + dtime);
-       }
+       m_uptime_counter->increment(dtime);
 
        handlePeerChanges();
 
@@ -524,11 +629,13 @@ void Server::AsyncRunStep(bool initial_step)
        */
 
        m_time_of_day_send_timer -= dtime;
-       if(m_time_of_day_send_timer < 0.0) {
+       if (m_time_of_day_send_timer < 0.0) {
                m_time_of_day_send_timer = g_settings->getFloat("time_send_interval");
                u16 time = m_env->getTimeOfDay();
                float time_speed = g_settings->getFloat("time_speed");
                SendTimeOfDay(PEER_ID_INEXISTENT, time, time_speed);
+
+               m_timeofday_gauge->set(time);
        }
 
        {
@@ -543,6 +650,7 @@ void Server::AsyncRunStep(bool initial_step)
                        max_lag = dtime;
                }
                m_env->reportMaxLagEstimate(max_lag);
+
                // Step environment
                m_env->step(dtime);
        }
@@ -554,8 +662,8 @@ void Server::AsyncRunStep(bool initial_step)
                // Run Map's timers and unload unused data
                ScopeProfiler sp(g_profiler, "Server: map timer and unload");
                m_env->getMap().timerUpdate(map_timer_and_unload_dtime,
-                       g_settings->getFloat("server_unload_unused_data_timeout"),
-                       U32_MAX);
+                       std::max(g_settings->getFloat("server_unload_unused_data_timeout"), 0.0f),
+                       -1);
        }
 
        /*
@@ -589,18 +697,34 @@ void Server::AsyncRunStep(bool initial_step)
                ScopeProfiler sp(g_profiler, "Server: liquid transform");
 
                std::map<v3s16, MapBlock*> modified_blocks;
-               m_env->getMap().transformLiquids(modified_blocks, m_env);
+               m_env->getServerMap().transformLiquids(modified_blocks, m_env);
 
-               /*
-                       Set the modified blocks unsent for all the clients
-               */
                if (!modified_blocks.empty()) {
-                       SetBlocksNotSent(modified_blocks);
+                       MapEditEvent event;
+                       event.type = MEET_OTHER;
+                       event.setModifiedBlocks(modified_blocks);
+                       m_env->getMap().dispatchEvent(event);
                }
        }
        m_clients.step(dtime);
 
-       m_lag += (m_lag > dtime ? -1 : 1) * dtime/100;
+       // increase/decrease lag gauge gradually
+       if (m_lag_gauge->get() > dtime) {
+               m_lag_gauge->decrement(dtime/100);
+       } else {
+               m_lag_gauge->increment(dtime/100);
+       }
+
+       {
+               float &counter = m_step_pending_dyn_media_timer;
+               counter += dtime;
+               if (counter >= 5.0f) {
+                       stepPendingDynMediaCallbacks(counter);
+                       counter = 0;
+               }
+       }
+
+
 #if USE_CURL
        // send masterserver announce
        {
@@ -611,9 +735,9 @@ void Server::AsyncRunStep(bool initial_step)
                                                ServerList::AA_START,
                                        m_bind_addr.getPort(),
                                        m_clients.getPlayerNames(),
-                                       m_uptime.get(),
+                                       m_uptime_counter->get(),
                                        m_env->getGameTime(),
-                                       m_lag,
+                                       m_lag_gauge->get(),
                                        m_gamespec.id,
                                        Mapgen::getMapgenName(m_emerge->mgparams->mgtype),
                                        m_modmgr->getMods(),
@@ -631,39 +755,36 @@ void Server::AsyncRunStep(bool initial_step)
                //infostream<<"Server: Checking added and deleted active objects"<<std::endl;
                MutexAutoLock envlock(m_env_mutex);
 
-               m_clients.lock();
-               const RemoteClientMap &clients = m_clients.getClientList();
-               ScopeProfiler sp(g_profiler, "Server: update objects within range");
+               {
+                       ClientInterface::AutoLock clientlock(m_clients);
+                       const RemoteClientMap &clients = m_clients.getClientList();
+                       ScopeProfiler sp(g_profiler, "Server: update objects within range");
 
-               for (const auto &client_it : clients) {
-                       RemoteClient *client = client_it.second;
+                       m_player_gauge->set(clients.size());
+                       for (const auto &client_it : clients) {
+                               RemoteClient *client = client_it.second;
 
-                       if (client->getState() < CS_DefinitionsSent)
-                               continue;
+                               if (client->getState() < CS_DefinitionsSent)
+                                       continue;
 
-                       // This can happen if the client times out somehow
-                       if (!m_env->getPlayer(client->peer_id))
-                               continue;
+                               // This can happen if the client times out somehow
+                               if (!m_env->getPlayer(client->peer_id))
+                                       continue;
 
-                       PlayerSAO *playersao = getPlayerSAO(client->peer_id);
-                       if (!playersao)
-                               continue;
+                               PlayerSAO *playersao = getPlayerSAO(client->peer_id);
+                               if (!playersao)
+                                       continue;
 
-                       SendActiveObjectRemoveAdd(client, playersao);
+                               SendActiveObjectRemoveAdd(client, playersao);
+                       }
                }
-               m_clients.unlock();
 
-               // Save mod storages if modified
+               // Write changes to the mod storage
                m_mod_storage_save_timer -= dtime;
                if (m_mod_storage_save_timer <= 0.0f) {
-                       infostream << "Saving registered mod storages." << std::endl;
                        m_mod_storage_save_timer = g_settings->getFloat("server_map_save_interval");
-                       for (std::unordered_map<std::string, ModMetadata *>::const_iterator
-                               it = m_mod_storages.begin(); it != m_mod_storages.end(); ++it) {
-                               if (it->second->isModified()) {
-                                       it->second->save(getModStoragePath());
-                               }
-                       }
+                       m_mod_storage_database->endSave();
+                       m_mod_storage_database->beginSave();
                }
        }
 
@@ -679,84 +800,88 @@ void Server::AsyncRunStep(bool initial_step)
                std::unordered_map<u16, std::vector<ActiveObjectMessage>*> buffered_messages;
 
                // Get active object messages from environment
+               ActiveObjectMessage aom(0);
+               u32 count_reliable = 0, count_unreliable = 0;
                for(;;) {
-                       ActiveObjectMessage aom = m_env->getActiveObjectMessage();
-                       if (aom.id == 0)
+                       if (!m_env->getActiveObjectMessage(&aom))
                                break;
+                       if (aom.reliable)
+                               count_reliable++;
+                       else
+                               count_unreliable++;
 
                        std::vector<ActiveObjectMessage>* message_list = nullptr;
-                       std::unordered_map<u16, std::vector<ActiveObjectMessage>* >::iterator n;
-                       n = buffered_messages.find(aom.id);
+                       auto n = buffered_messages.find(aom.id);
                        if (n == buffered_messages.end()) {
                                message_list = new std::vector<ActiveObjectMessage>;
                                buffered_messages[aom.id] = message_list;
-                       }
-                       else {
+                       } else {
                                message_list = n->second;
                        }
-                       message_list->push_back(aom);
-               }
-
-               m_clients.lock();
-               const RemoteClientMap &clients = m_clients.getClientList();
-               // Route data to every client
-               for (const auto &client_it : clients) {
-                       RemoteClient *client = client_it.second;
-                       PlayerSAO *player = getPlayerSAO(client->peer_id);
-                       std::string reliable_data;
-                       std::string unreliable_data;
-                       // Go through all objects in message buffer
-                       for (const auto &buffered_message : buffered_messages) {
-                               // If object does not exist or is not known by client, skip it
-                               u16 id = buffered_message.first;
-                               ServerActiveObject *sao = m_env->getActiveObject(id);
-                               if (!sao || client->m_known_objects.find(id) == client->m_known_objects.end())
-                                       continue;
+                       message_list->push_back(std::move(aom));
+               }
 
-                               // Get message list of object
-                               std::vector<ActiveObjectMessage>* list = buffered_message.second;
-                               // Go through every message
-                               for (const ActiveObjectMessage &aom : *list) {
-                                       // Send position updates to players who do not see the attachment
-                                       if (aom.datastring[0] == GENERIC_CMD_UPDATE_POSITION) {
-                                               if (sao->getId() == player->getId())
-                                                       continue;
-
-                                               // Do not send position updates for attached players
-                                               // as long the parent is known to the client
-                                               ServerActiveObject *parent = sao->getParent();
-                                               if (parent && client->m_known_objects.find(parent->getId()) !=
-                                                               client->m_known_objects.end())
-                                                       continue;
+               m_aom_buffer_counter[0]->increment(count_reliable);
+               m_aom_buffer_counter[1]->increment(count_unreliable);
+
+               {
+                       ClientInterface::AutoLock clientlock(m_clients);
+                       const RemoteClientMap &clients = m_clients.getClientList();
+                       // Route data to every client
+                       std::string reliable_data, unreliable_data;
+                       for (const auto &client_it : clients) {
+                               reliable_data.clear();
+                               unreliable_data.clear();
+                               RemoteClient *client = client_it.second;
+                               PlayerSAO *player = getPlayerSAO(client->peer_id);
+                               // Go through all objects in message buffer
+                               for (const auto &buffered_message : buffered_messages) {
+                                       // If object does not exist or is not known by client, skip it
+                                       u16 id = buffered_message.first;
+                                       ServerActiveObject *sao = m_env->getActiveObject(id);
+                                       if (!sao || client->m_known_objects.find(id) == client->m_known_objects.end())
+                                               continue;
+
+                                       // Get message list of object
+                                       std::vector<ActiveObjectMessage>* list = buffered_message.second;
+                                       // Go through every message
+                                       for (const ActiveObjectMessage &aom : *list) {
+                                               // Send position updates to players who do not see the attachment
+                                               if (aom.datastring[0] == AO_CMD_UPDATE_POSITION) {
+                                                       if (sao->getId() == player->getId())
+                                                               continue;
+
+                                                       // Do not send position updates for attached players
+                                                       // as long the parent is known to the client
+                                                       ServerActiveObject *parent = sao->getParent();
+                                                       if (parent && client->m_known_objects.find(parent->getId()) !=
+                                                                       client->m_known_objects.end())
+                                                               continue;
+                                               }
+
+                                               // Add full new data to appropriate buffer
+                                               std::string &buffer = aom.reliable ? reliable_data : unreliable_data;
+                                               char idbuf[2];
+                                               writeU16((u8*) idbuf, aom.id);
+                                               // u16 id
+                                               // std::string data
+                                               buffer.append(idbuf, sizeof(idbuf));
+                                               buffer.append(serializeString16(aom.datastring));
                                        }
-                                       // Compose the full new data with header
-                                       std::string new_data;
-                                       // Add object id
-                                       char buf[2];
-                                       writeU16((u8*)&buf[0], aom.id);
-                                       new_data.append(buf, 2);
-                                       // Add data
-                                       new_data += serializeString(aom.datastring);
-                                       // Add data to buffer
-                                       if (aom.reliable)
-                                               reliable_data += new_data;
-                                       else
-                                               unreliable_data += new_data;
                                }
-                       }
-                       /*
-                               reliable_data and unreliable_data are now ready.
-                               Send them.
-                       */
-                       if (!reliable_data.empty()) {
-                               SendActiveObjectMessages(client->peer_id, reliable_data);
-                       }
+                               /*
+                                       reliable_data and unreliable_data are now ready.
+                                       Send them.
+                               */
+                               if (!reliable_data.empty()) {
+                                       SendActiveObjectMessages(client->peer_id, reliable_data);
+                               }
 
-                       if (!unreliable_data.empty()) {
-                               SendActiveObjectMessages(client->peer_id, unreliable_data, false);
+                               if (!unreliable_data.empty()) {
+                                       SendActiveObjectMessages(client->peer_id, unreliable_data, false);
+                               }
                        }
                }
-               m_clients.unlock();
 
                // Clear buffered_messages
                for (auto &buffered_message : buffered_messages) {
@@ -771,20 +896,18 @@ void Server::AsyncRunStep(bool initial_step)
                // We will be accessing the environment
                MutexAutoLock lock(m_env_mutex);
 
-               // Don't send too many at a time
-               //u32 count = 0;
-
-               // Single change sending is disabled if queue size is not small
+               // Single change sending is disabled if queue size is big
                bool disable_single_change_sending = false;
                if(m_unsent_map_edit_queue.size() >= 4)
                        disable_single_change_sending = true;
 
-               int event_count = m_unsent_map_edit_queue.size();
+               const auto event_count = m_unsent_map_edit_queue.size();
+               m_map_edit_event_counter->increment(event_count);
 
                // We'll log the amount of each
                Profiler prof;
 
-               std::list<v3s16> node_meta_updates;
+               std::unordered_set<v3s16> node_meta_updates;
 
                while (!m_unsent_map_edit_queue.empty()) {
                        MapEditEvent* event = m_unsent_map_edit_queue.front();
@@ -809,12 +932,9 @@ void Server::AsyncRunStep(bool initial_step)
                                                disable_single_change_sending ? 5 : 30);
                                break;
                        case MEET_BLOCK_NODE_METADATA_CHANGED: {
-                               verbosestream << "Server: MEET_BLOCK_NODE_METADATA_CHANGED" << std::endl;
                                prof.add("MEET_BLOCK_NODE_METADATA_CHANGED", 1);
                                if (!event->is_private_change) {
-                                       // Don't send the change yet. Collect them to eliminate dupes.
-                                       node_meta_updates.remove(event->p);
-                                       node_meta_updates.push_back(event->p);
+                                       node_meta_updates.emplace(event->p);
                                }
 
                                if (MapBlock *block = m_env->getMap().getBlockNoCreateNoEx(
@@ -825,7 +945,6 @@ void Server::AsyncRunStep(bool initial_step)
                                break;
                        }
                        case MEET_OTHER:
-                               infostream << "Server: MEET_OTHER" << std::endl;
                                prof.add("MEET_OTHER", 1);
                                for (const v3s16 &modified_block : event->modified_blocks) {
                                        m_clients.markBlockposAsNotSent(modified_block);
@@ -868,19 +987,19 @@ void Server::AsyncRunStep(bool initial_step)
                }
 
                // Send all metadata updates
-               if (node_meta_updates.size())
+               if (!node_meta_updates.empty())
                        sendMetadataChanged(node_meta_updates);
        }
 
        /*
-               Trigger emergethread (it somehow gets to a non-triggered but
-               bysy state sometimes)
+               Trigger emerge thread
+               Doing this every 2s is left over from old code, unclear if this is still needed.
        */
        {
                float &counter = m_emergethread_trigger_timer;
-               counter += dtime;
-               if (counter >= 2.0) {
-                       counter = 0.0;
+               counter -= dtime;
+               if (counter <= 0.0f) {
+                       counter = 2.0f;
 
                        m_emerge->startThreads();
                }
@@ -939,7 +1058,9 @@ void Server::Receive()
                        }
 
                        peer_id = pkt.getPeerId();
+                       m_packet_recv_counter->increment();
                        ProcessData(&pkt);
+                       m_packet_recv_processed_counter->increment();
                } catch (const con::InvalidIncomingDataException &e) {
                        infostream << "Server::Receive(): InvalidIncomingDataException: what()="
                                        << e.what() << std::endl;
@@ -949,8 +1070,7 @@ void Server::Receive()
                } catch (const ClientStateError &e) {
                        errorstream << "ProcessData: peer=" << peer_id << " what()="
                                         << e.what() << std::endl;
-                       DenyAccess_Legacy(peer_id, L"Your client sent something server didn't expect."
-                                       L"Try reconnecting or updating your client");
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                } catch (const con::PeerNotFoundException &e) {
                        // Do nothing
                } catch (const con::NoIncomingDataException &e) {
@@ -963,18 +1083,14 @@ PlayerSAO* Server::StageTwoClientInit(session_t peer_id)
 {
        std::string playername;
        PlayerSAO *playersao = NULL;
-       m_clients.lock();
-       try {
+       {
+               ClientInterface::AutoLock clientlock(m_clients);
                RemoteClient* client = m_clients.lockedGetClientNoEx(peer_id, CS_InitDone);
                if (client) {
                        playername = client->getName();
                        playersao = emergePlayer(playername.c_str(), peer_id, client->net_proto_version);
                }
-       } catch (std::exception &e) {
-               m_clients.unlock();
-               throw;
        }
-       m_clients.unlock();
 
        RemotePlayer *player = m_env->getPlayer(playername.c_str());
 
@@ -982,16 +1098,14 @@ PlayerSAO* Server::StageTwoClientInit(session_t peer_id)
        if (!playersao || !player) {
                if (player && player->getPeerId() != PEER_ID_INEXISTENT) {
                        actionstream << "Server: Failed to emerge player \"" << playername
-                                       << "\" (player allocated to an another client)" << std::endl;
-                       DenyAccess_Legacy(peer_id, L"Another client is connected with this "
-                                       L"name. If your client closed unexpectedly, try again in "
-                                       L"a minute.");
+                                       << "\" (player allocated to another client)" << std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_ALREADY_CONNECTED);
                } else {
                        errorstream << "Server: " << playername << ": Failed to emerge player"
                                        << std::endl;
-                       DenyAccess_Legacy(peer_id, L"Could not allocate player.");
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_SERVER_FAIL);
                }
-               return NULL;
+               return nullptr;
        }
 
        /*
@@ -1008,32 +1122,32 @@ PlayerSAO* Server::StageTwoClientInit(session_t peer_id)
        // Send inventory
        SendInventory(playersao, false);
 
-       // Send HP or death screen
+       // Send HP
+       SendPlayerHP(playersao, false);
+
+       // Send death screen
        if (playersao->isDead())
                SendDeathscreen(peer_id, false, v3f(0,0,0));
-       else
-               SendPlayerHPOrDie(playersao,
-                               PlayerHPChangeReason(PlayerHPChangeReason::SET_HP));
 
        // Send Breath
        SendPlayerBreath(playersao);
 
-       Address addr = getPeerAddress(player->getPeerId());
-       std::string ip_str = addr.serializeString();
-       actionstream<<player->getName() <<" [" << ip_str << "] joins game. " << std::endl;
        /*
-               Print out action
+               Update player list and print action
        */
        {
-               const std::vector<std::string> &names = m_clients.getPlayerNames();
-
-               actionstream << player->getName() << " joins game. List of players: ";
+               NetworkPacket notice_pkt(TOCLIENT_UPDATE_PLAYER_LIST, 0, PEER_ID_INEXISTENT);
+               notice_pkt << (u8) PLAYER_LIST_ADD << (u16) 1 << std::string(player->getName());
+               m_clients.sendToAll(&notice_pkt);
+       }
+       {
+               std::string ip_str = getPeerAddress(player->getPeerId()).serializeString();
+               const auto &names = m_clients.getPlayerNames();
 
-               for (const std::string &name : names) {
+               actionstream << player->getName() << " [" << ip_str << "] joins game. List of players: ";
+               for (const std::string &name : names)
                        actionstream << name << " ";
-               }
-
-               actionstream << player->getName() <<std::endl;
+               actionstream << player->getName() << std::endl;
        }
        return playersao;
 }
@@ -1056,18 +1170,16 @@ void Server::ProcessData(NetworkPacket *pkt)
                Address address = getPeerAddress(peer_id);
                std::string addr_s = address.serializeString();
 
-               if(m_banmanager->isIpBanned(addr_s)) {
+               // FIXME: Isn't it a bit excessive to check this for every packet?
+               if (m_banmanager->isIpBanned(addr_s)) {
                        std::string ban_name = m_banmanager->getBanName(addr_s);
                        infostream << "Server: A banned client tried to connect from "
-                                       << addr_s << "; banned name was "
-                                       << ban_name << std::endl;
-                       // This actually doesn't seem to transfer to the client
-                       DenyAccess_Legacy(peer_id, L"Your ip is banned. Banned name was "
-                                       + utf8_to_wide(ban_name));
+                                       << addr_s << "; banned name was " << ban_name << std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_CUSTOM_STRING,
+                               "Your IP is banned. Banned name was " + ban_name);
                        return;
                }
-       }
-       catch(con::PeerNotFoundException &e) {
+       } catch (con::PeerNotFoundException &e) {
                /*
                 * no peer for this packet found
                 * most common reason is peer timeout, e.g. peer didn't
@@ -1144,94 +1256,6 @@ void Server::onMapEditEvent(const MapEditEvent &event)
        m_unsent_map_edit_queue.push(new MapEditEvent(event));
 }
 
-Inventory* Server::getInventory(const InventoryLocation &loc)
-{
-       switch (loc.type) {
-       case InventoryLocation::UNDEFINED:
-       case InventoryLocation::CURRENT_PLAYER:
-               break;
-       case InventoryLocation::PLAYER:
-       {
-               RemotePlayer *player = m_env->getPlayer(loc.name.c_str());
-               if(!player)
-                       return NULL;
-               PlayerSAO *playersao = player->getPlayerSAO();
-               if(!playersao)
-                       return NULL;
-               return playersao->getInventory();
-       }
-               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:
-               sanity_check(false); // abort
-               break;
-       }
-       return NULL;
-}
-
-void Server::setInventoryModified(const InventoryLocation &loc)
-{
-       switch(loc.type){
-       case InventoryLocation::UNDEFINED:
-               break;
-       case InventoryLocation::PLAYER:
-       {
-
-               RemotePlayer *player = m_env->getPlayer(loc.name.c_str());
-
-               if (!player)
-                       return;
-
-               player->setModified(true);
-               player->inventory.setModified(true);
-               // Updates are sent in ServerEnvironment::step()
-       }
-               break;
-       case InventoryLocation::NODEMETA:
-       {
-               MapEditEvent event;
-               event.type = MEET_BLOCK_NODE_METADATA_CHANGED;
-               event.p = loc.p;
-               m_env->getMap().dispatchEvent(event);
-       }
-               break;
-       case InventoryLocation::DETACHED:
-       {
-               // Updates are sent in ServerEnvironment::step()
-       }
-               break;
-       default:
-               sanity_check(false); // abort
-               break;
-       }
-}
-
-void Server::SetBlocksNotSent(std::map<v3s16, MapBlock *>& block)
-{
-       std::vector<session_t> clients = m_clients.getClientIDs();
-       m_clients.lock();
-       // Set the modified blocks unsent for all the clients
-       for (const session_t client_id : clients) {
-                       if (RemoteClient *client = m_clients.lockedGetClientNoEx(client_id))
-                               client->SetBlocksNotSent(block);
-       }
-       m_clients.unlock();
-}
-
 void Server::peerAdded(con::Peer *peer)
 {
        verbosestream<<"Server::peerAdded(): peer->id="
@@ -1255,37 +1279,26 @@ bool Server::getClientConInfo(session_t peer_id, con::rtt_stat_type type, float*
        return *retval != -1;
 }
 
-bool Server::getClientInfo(
-               session_t    peer_id,
-               ClientState* state,
-               u32*         uptime,
-               u8*          ser_vers,
-               u16*         prot_vers,
-               u8*          major,
-               u8*          minor,
-               u8*          patch,
-               std::string* vers_string
-       )
-{
-       *state = m_clients.getClientState(peer_id);
-       m_clients.lock();
+bool Server::getClientInfo(session_t peer_id, ClientInfo &ret)
+{
+       ClientInterface::AutoLock clientlock(m_clients);
        RemoteClient* client = m_clients.lockedGetClientNoEx(peer_id, CS_Invalid);
 
-       if (!client) {
-               m_clients.unlock();
+       if (!client)
                return false;
-       }
 
-       *uptime = client->uptime();
-       *ser_vers = client->serialization_version;
-       *prot_vers = client->net_proto_version;
+       ret.state = client->getState();
+       ret.addr = client->getAddress();
+       ret.uptime = client->uptime();
+       ret.ser_vers = client->serialization_version;
+       ret.prot_vers = client->net_proto_version;
 
-       *major = client->getMajor();
-       *minor = client->getMinor();
-       *patch = client->getPatch();
-       *vers_string = client->getFull();
+       ret.major = client->getMajor();
+       ret.minor = client->getMinor();
+       ret.patch = client->getPatch();
+       ret.vers_string = client->getFullVer();
 
-       m_clients.unlock();
+       ret.lang_code = client->getLangCode();
 
        return true;
 }
@@ -1343,8 +1356,6 @@ void Server::Send(session_t peer_id, NetworkPacket *pkt)
 
 void Server::SendMovement(session_t peer_id)
 {
-       std::ostringstream os(std::ios_base::binary);
-
        NetworkPacket pkt(TOCLIENT_MOVEMENT, 12 * sizeof(float), peer_id);
 
        pkt << g_settings->getFloat("movement_acceleration_default");
@@ -1363,24 +1374,27 @@ void Server::SendMovement(session_t peer_id)
        Send(&pkt);
 }
 
-void Server::SendPlayerHPOrDie(PlayerSAO *playersao, const PlayerHPChangeReason &reason)
+void Server::HandlePlayerHPChange(PlayerSAO *playersao, const PlayerHPChangeReason &reason)
 {
-       if (playersao->isImmortal())
-               return;
+       m_script->player_event(playersao, "health_changed");
+       SendPlayerHP(playersao, reason.type != PlayerHPChangeReason::SET_HP_MAX);
 
-       session_t peer_id = playersao->getPeerID();
-       bool is_alive = playersao->getHP() > 0;
+       // Send to other clients
+       playersao->sendPunchCommand();
 
-       if (is_alive)
-               SendPlayerHP(peer_id);
-       else
-               DiePlayer(peer_id, reason);
+       if (playersao->isDead())
+               HandlePlayerDeath(playersao, reason);
+}
+
+void Server::SendPlayerHP(PlayerSAO *playersao, bool effect)
+{
+       SendHP(playersao->getPeerID(), playersao->getHP(), effect);
 }
 
-void Server::SendHP(session_t peer_id, u16 hp)
+void Server::SendHP(session_t peer_id, u16 hp, bool effect)
 {
-       NetworkPacket pkt(TOCLIENT_HP, 1, peer_id);
-       pkt << hp;
+       NetworkPacket pkt(TOCLIENT_HP, 3, peer_id);
+       pkt << hp << effect;
        Send(&pkt);
 }
 
@@ -1406,13 +1420,6 @@ void Server::SendAccessDenied(session_t peer_id, AccessDeniedCode reason,
        Send(&pkt);
 }
 
-void Server::SendAccessDenied_Legacy(session_t peer_id,const std::wstring &reason)
-{
-       NetworkPacket pkt(TOCLIENT_ACCESS_DENIED_LEGACY, 0, peer_id);
-       pkt << reason;
-       Send(&pkt);
-}
-
 void Server::SendDeathscreen(session_t peer_id, bool set_camera_point_target,
                v3f camera_point_target)
 {
@@ -1502,7 +1509,8 @@ void Server::SendChatMessage(session_t peer_id, const ChatMessage &message)
        NetworkPacket pkt(TOCLIENT_CHAT_MESSAGE, 0, peer_id);
        u8 version = 1;
        u8 type = message.type;
-       pkt << version << type << std::wstring(L"") << message.message << (u64)message.timestamp;
+       pkt << version << type << message.sender << message.message
+               << static_cast<u64>(message.timestamp);
 
        if (peer_id != PEER_ID_INEXISTENT) {
                RemotePlayer *player = m_env->getPlayer(peer_id);
@@ -1538,17 +1546,15 @@ void Server::SendShowFormspecMessage(session_t peer_id, const std::string &forms
 
 // Spawns a particle on peer with peer_id
 void Server::SendSpawnParticle(session_t peer_id, u16 protocol_version,
-                               v3f pos, v3f velocity, v3f acceleration,
-                               float expirationtime, float size, bool collisiondetection,
-                               bool collision_removal, bool object_collision,
-                               bool vertical, const std::string &texture,
-                               const struct TileAnimationParams &animation, u8 glow)
+       const ParticleParameters &p)
 {
        static thread_local const float radius =
                        g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE * BS;
 
        if (peer_id == PEER_ID_INEXISTENT) {
                std::vector<session_t> clients = m_clients.getClientIDs();
+               const v3f pos = p.pos * BS;
+               const float radius_sq = radius * radius;
 
                for (const session_t client_id : clients) {
                        RemotePlayer *player = m_env->getPlayer(client_id);
@@ -1560,76 +1566,136 @@ void Server::SendSpawnParticle(session_t peer_id, u16 protocol_version,
                                continue;
 
                        // Do not send to distant clients
-                       if (sao->getBasePosition().getDistanceFrom(pos * BS) > radius)
+                       if (sao->getBasePosition().getDistanceFromSQ(pos) > radius_sq)
                                continue;
 
-                       SendSpawnParticle(client_id, player->protocol_version,
-                                       pos, velocity, acceleration,
-                                       expirationtime, size, collisiondetection, collision_removal,
-                                       object_collision, vertical, texture, animation, glow);
+                       SendSpawnParticle(client_id, player->protocol_version, p);
                }
                return;
        }
+       assert(protocol_version != 0);
 
        NetworkPacket pkt(TOCLIENT_SPAWN_PARTICLE, 0, peer_id);
 
-       pkt << pos << velocity << acceleration << expirationtime
-                       << size << collisiondetection;
-       pkt.putLongString(texture);
-       pkt << vertical;
-       pkt << collision_removal;
-       // This is horrible but required (why are there two ways to serialize pkts?)
-       std::ostringstream os(std::ios_base::binary);
-       animation.serialize(os, protocol_version);
-       pkt.putRawString(os.str());
-       pkt << glow;
-       pkt << object_collision;
+       {
+               // NetworkPacket and iostreams are incompatible...
+               std::ostringstream oss(std::ios_base::binary);
+               p.serialize(oss, protocol_version);
+               pkt.putRawString(oss.str());
+       }
 
        Send(&pkt);
 }
 
 // Adds a ParticleSpawner on peer with peer_id
 void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version,
-       u16 amount, float spawntime, v3f minpos, v3f maxpos,
-       v3f minvel, v3f maxvel, v3f minacc, v3f maxacc, float minexptime, float maxexptime,
-       float minsize, float maxsize, bool collisiondetection, bool collision_removal,
-       bool object_collision, u16 attached_id, bool vertical, const std::string &texture, u32 id,
-       const struct TileAnimationParams &animation, u8 glow)
+       const ParticleSpawnerParameters &p, u16 attached_id, u32 id)
 {
+       static thread_local const float radius =
+                       g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE * BS;
+
        if (peer_id == PEER_ID_INEXISTENT) {
-               // This sucks and should be replaced:
                std::vector<session_t> clients = m_clients.getClientIDs();
+               const v3f pos = (
+                       p.pos.start.min.val +
+                       p.pos.start.max.val +
+                       p.pos.end.min.val +
+                       p.pos.end.max.val
+               ) / 4.0f * BS;
+               const float radius_sq = radius * radius;
+               /* Don't send short-lived spawners to distant players.
+                * This could be replaced with proper tracking at some point. */
+               const bool distance_check = !attached_id && p.time <= 1.0f;
+
                for (const session_t client_id : clients) {
                        RemotePlayer *player = m_env->getPlayer(client_id);
                        if (!player)
                                continue;
+
+                       if (distance_check) {
+                               PlayerSAO *sao = player->getPlayerSAO();
+                               if (!sao)
+                                       continue;
+                               if (sao->getBasePosition().getDistanceFromSQ(pos) > radius_sq)
+                                       continue;
+                       }
+
                        SendAddParticleSpawner(client_id, player->protocol_version,
-                                       amount, spawntime, minpos, maxpos,
-                                       minvel, maxvel, minacc, maxacc, minexptime, maxexptime,
-                                       minsize, maxsize, collisiondetection, collision_removal,
-                                       object_collision, attached_id, vertical, texture, id,
-                                       animation, glow);
+                               p, attached_id, id);
                }
                return;
        }
+       assert(protocol_version != 0);
 
-       NetworkPacket pkt(TOCLIENT_ADD_PARTICLESPAWNER, 0, peer_id);
+       NetworkPacket pkt(TOCLIENT_ADD_PARTICLESPAWNER, 100, peer_id);
 
-       pkt << amount << spawntime << minpos << maxpos << minvel << maxvel
-                       << minacc << maxacc << minexptime << maxexptime << minsize
-                       << maxsize << collisiondetection;
+       pkt << p.amount << p.time;
+       { // serialize legacy fields
+               std::ostringstream os(std::ios_base::binary);
+               p.pos.start.legacySerialize(os);
+               p.vel.start.legacySerialize(os);
+               p.acc.start.legacySerialize(os);
+               p.exptime.start.legacySerialize(os);
+               p.size.start.legacySerialize(os);
+               pkt.putRawString(os.str());
+       }
+       pkt << p.collisiondetection;
 
-       pkt.putLongString(texture);
+       pkt.putLongString(p.texture.string);
 
-       pkt << id << vertical;
-       pkt << collision_removal;
-       pkt << attached_id;
-       // This is horrible but required
-       std::ostringstream os(std::ios_base::binary);
-       animation.serialize(os, protocol_version);
-       pkt.putRawString(os.str());
-       pkt << glow;
-       pkt << object_collision;
+       pkt << id << p.vertical << p.collision_removal << attached_id;
+       {
+               std::ostringstream os(std::ios_base::binary);
+               p.animation.serialize(os, protocol_version);
+               pkt.putRawString(os.str());
+       }
+       pkt << p.glow << p.object_collision;
+       pkt << p.node.param0 << p.node.param2 << p.node_tile;
+
+       { // serialize new fields
+               // initial bias for older properties
+               pkt << p.pos.start.bias
+                       << p.vel.start.bias
+                       << p.acc.start.bias
+                       << p.exptime.start.bias
+                       << p.size.start.bias;
+
+               std::ostringstream os(std::ios_base::binary);
+
+               // final tween frames of older properties
+               p.pos.end.serialize(os);
+               p.vel.end.serialize(os);
+               p.acc.end.serialize(os);
+               p.exptime.end.serialize(os);
+               p.size.end.serialize(os);
+
+               // properties for legacy texture field
+               p.texture.serialize(os, protocol_version, true);
+
+               // new properties
+               p.drag.serialize(os);
+               p.jitter.serialize(os);
+               p.bounce.serialize(os);
+               ParticleParamTypes::serializeParameterValue(os, p.attractor_kind);
+               if (p.attractor_kind != ParticleParamTypes::AttractorKind::none) {
+                       p.attract.serialize(os);
+                       p.attractor_origin.serialize(os);
+                       writeU16(os, p.attractor_attachment); /* object ID */
+                       writeU8(os, p.attractor_kill);
+                       if (p.attractor_kind != ParticleParamTypes::AttractorKind::point) {
+                               p.attractor_direction.serialize(os);
+                               writeU16(os, p.attractor_direction_attachment);
+                       }
+               }
+               p.radius.serialize(os);
+
+               ParticleParamTypes::serializeParameterValue(os, (u16)p.texpool.size());
+               for (const auto& tex : p.texpool) {
+                       tex.serialize(os, protocol_version);
+               }
+
+               pkt.putRawString(os.str());
+       }
 
        Send(&pkt);
 }
@@ -1638,7 +1704,6 @@ void Server::SendDeleteParticleSpawner(session_t peer_id, u32 id)
 {
        NetworkPacket pkt(TOCLIENT_DELETE_PARTICLESPAWNER, 4, peer_id);
 
-       // Ugly error in this packet
        pkt << id;
 
        if (peer_id != PEER_ID_INEXISTENT)
@@ -1655,7 +1720,7 @@ void Server::SendHUDAdd(session_t peer_id, u32 id, HudElement *form)
        pkt << id << (u8) form->type << form->pos << form->name << form->scale
                        << form->text << form->number << form->item << form->dir
                        << form->align << form->offset << form->world_pos << form->size
-                       << form->z_index;
+                       << form->z_index << form->text2 << form->style;
 
        Send(&pkt);
 }
@@ -1681,6 +1746,7 @@ void Server::SendHUDChange(session_t peer_id, u32 id, HudElementStat stat, void
                        break;
                case HUD_STAT_NAME:
                case HUD_STAT_TEXT:
+               case HUD_STAT_TEXT2:
                        pkt << *(std::string *) value;
                        break;
                case HUD_STAT_WORLD_POS:
@@ -1689,10 +1755,7 @@ void Server::SendHUDChange(session_t peer_id, u32 id, HudElementStat stat, void
                case HUD_STAT_SIZE:
                        pkt << *(v2s32 *) value;
                        break;
-               case HUD_STAT_NUMBER:
-               case HUD_STAT_ITEM:
-               case HUD_STAT_DIR:
-               default:
+               default: // all other types
                        pkt << *(u32 *) value;
                        break;
        }
@@ -1718,17 +1781,63 @@ void Server::SendHUDSetParam(session_t peer_id, u16 param, const std::string &va
        Send(&pkt);
 }
 
-void Server::SendSetSky(session_t peer_id, const video::SColor &bgcolor,
-               const std::string &type, const std::vector<std::string> &params,
-               bool &clouds)
+void Server::SendSetSky(session_t peer_id, const SkyboxParams &params)
 {
        NetworkPacket pkt(TOCLIENT_SET_SKY, 0, peer_id);
-       pkt << bgcolor << type << (u16) params.size();
 
-       for (const std::string &param : params)
-               pkt << param;
+       // Handle prior clients here
+       if (m_clients.getProtocolVersion(peer_id) < 39) {
+               pkt << params.bgcolor << params.type << (u16) params.textures.size();
+
+               for (const std::string& texture : params.textures)
+                       pkt << texture;
+
+               pkt << params.clouds;
+       } else { // Handle current clients and future clients
+               pkt << params.bgcolor << params.type
+               << params.clouds << params.fog_sun_tint
+               << params.fog_moon_tint << params.fog_tint_type;
+
+               if (params.type == "skybox") {
+                       pkt << (u16) params.textures.size();
+                       for (const std::string &texture : params.textures)
+                               pkt << texture;
+               } else if (params.type == "regular") {
+                       pkt << params.sky_color.day_sky << params.sky_color.day_horizon
+                               << params.sky_color.dawn_sky << params.sky_color.dawn_horizon
+                               << params.sky_color.night_sky << params.sky_color.night_horizon
+                               << params.sky_color.indoors;
+               }
+       }
+
+       Send(&pkt);
+}
+
+void Server::SendSetSun(session_t peer_id, const SunParams &params)
+{
+       NetworkPacket pkt(TOCLIENT_SET_SUN, 0, peer_id);
+       pkt << params.visible << params.texture
+               << params.tonemap << params.sunrise
+               << params.sunrise_visible << params.scale;
+
+       Send(&pkt);
+}
+void Server::SendSetMoon(session_t peer_id, const MoonParams &params)
+{
+       NetworkPacket pkt(TOCLIENT_SET_MOON, 0, peer_id);
+
+       pkt << params.visible << params.texture
+               << params.tonemap << params.scale;
+
+       Send(&pkt);
+}
+void Server::SendSetStars(session_t peer_id, const StarParams &params)
+{
+       NetworkPacket pkt(TOCLIENT_SET_STARS, 0, peer_id);
 
-       pkt << clouds;
+       pkt << params.visible << params.count
+               << params.starcolor << params.scale
+               << params.day_opacity;
 
        Send(&pkt);
 }
@@ -1752,6 +1861,24 @@ void Server::SendOverrideDayNightRatio(session_t peer_id, bool do_override,
        Send(&pkt);
 }
 
+void Server::SendSetLighting(session_t peer_id, const Lighting &lighting)
+{
+       NetworkPacket pkt(TOCLIENT_SET_LIGHTING,
+                       4, peer_id);
+
+       pkt << lighting.shadow_intensity;
+       pkt << lighting.saturation;
+
+       pkt << lighting.exposure.luminance_min
+                       << lighting.exposure.luminance_max
+                       << lighting.exposure.exposure_correction
+                       << lighting.exposure.speed_dark_bright
+                       << lighting.exposure.speed_bright_dark
+                       << lighting.exposure.center_weight_power;
+
+       Send(&pkt);
+}
+
 void Server::SendTimeOfDay(session_t peer_id, u16 time, f32 time_speed)
 {
        NetworkPacket pkt(TOCLIENT_TIME_OF_DAY, 0, peer_id);
@@ -1765,23 +1892,6 @@ void Server::SendTimeOfDay(session_t peer_id, u16 time, f32 time_speed)
        }
 }
 
-void Server::SendPlayerHP(session_t peer_id)
-{
-       PlayerSAO *playersao = getPlayerSAO(peer_id);
-       // In some rare case if the player is disconnected
-       // while Lua call l_punch, for example, this can be NULL
-       if (!playersao)
-               return;
-
-       SendHP(peer_id, playersao->getHP());
-       m_script->player_event(playersao,"health_changed");
-
-       // Send to other clients
-       std::string str = gob_cmd_punched(playersao->getHP());
-       ActiveObjectMessage aom(playersao->getId(), true, str);
-       playersao->m_messages_out.push(aom);
-}
-
 void Server::SendPlayerBreath(PlayerSAO *sao)
 {
        assert(sao);
@@ -1797,6 +1907,9 @@ void Server::SendMovePlayer(session_t peer_id)
        PlayerSAO *sao = player->getPlayerSAO();
        assert(sao);
 
+       // Send attachment updates instantly to the client prior updating position
+       sao->sendOutdatedData();
+
        NetworkPacket pkt(TOCLIENT_MOVE_PLAYER, sizeof(v3f) + sizeof(f32) * 2, peer_id);
        pkt << sao->getBasePosition() << sao->getLookPitch() << sao->getRotation().Y;
 
@@ -1814,10 +1927,10 @@ void Server::SendMovePlayer(session_t peer_id)
 
 void Server::SendPlayerFov(session_t peer_id)
 {
-       NetworkPacket pkt(TOCLIENT_FOV, 4 + 1, peer_id);
+       NetworkPacket pkt(TOCLIENT_FOV, 4 + 1 + 4, peer_id);
 
        PlayerFovSpec fov_spec = m_env->getPlayer(peer_id)->getFov();
-       pkt << fov_spec.fov << fov_spec.is_multiplier;
+       pkt << fov_spec.fov << fov_spec.is_multiplier << fov_spec.transition_time;
 
        Send(&pkt);
 }
@@ -1967,7 +2080,7 @@ void Server::SendActiveObjectRemoveAdd(RemoteClient *client, PlayerSAO *playersa
                writeU8((u8*)buf, type);
                data.append(buf, 1);
 
-               data.append(serializeLongString(
+               data.append(serializeString32(
                        obj->getClientInitializationData(client->net_proto_version)));
 
                // Add to known objects
@@ -2023,14 +2136,13 @@ inline s32 Server::nextSoundId()
        return ret;
 }
 
-s32 Server::playSound(const SimpleSoundSpec &spec,
-               const ServerSoundParams &params, bool ephemeral)
+s32 Server::playSound(ServerPlayingSound &params, bool ephemeral)
 {
        // Find out initial position of sound
        bool pos_exists = false;
-       v3f pos = params.getPos(m_env, &pos_exists);
+       const v3f pos = params.getPos(m_env, &pos_exists);
        // If position is not found while it should be, cancel sound
-       if(pos_exists != (params.type != ServerSoundParams::SSP_LOCAL))
+       if(pos_exists != (params.type != SoundLocation::Local))
                return -1;
 
        // Filter destination clients
@@ -2075,154 +2187,100 @@ s32 Server::playSound(const SimpleSoundSpec &spec,
        if(dst_clients.empty())
                return -1;
 
-       // Create the sound
-       s32 id;
-       ServerPlayingSound *psound = nullptr;
-       if (ephemeral) {
-               id = -1; // old clients will still use this, so pick a reserved ID
-       } else {
-               id = nextSoundId();
-               // The sound will exist as a reference in m_playing_sounds
-               m_playing_sounds[id] = ServerPlayingSound();
-               psound = &m_playing_sounds[id];
-               psound->params = params;
-               psound->spec = spec;
-       }
+       // old clients will still use this, so pick a reserved ID (-1)
+       const s32 id = ephemeral ? -1 : nextSoundId();
 
-       float gain = params.gain * spec.gain;
+       float gain = params.gain * params.spec.gain;
        NetworkPacket pkt(TOCLIENT_PLAY_SOUND, 0);
-       pkt << id << spec.name << gain
+       pkt << id << params.spec.name << gain
                        << (u8) params.type << pos << params.object
-                       << params.loop << params.fade << params.pitch
+                       << params.spec.loop << params.spec.fade << params.spec.pitch
                        << ephemeral;
 
        bool as_reliable = !ephemeral;
 
-       for (const u16 dst_client : dst_clients) {
-               if (psound)
-                       psound->clients.insert(dst_client);
-               m_clients.send(dst_client, 0, &pkt, as_reliable);
+       for (const session_t peer_id : dst_clients) {
+               if (!ephemeral)
+                       params.clients.insert(peer_id);
+               m_clients.send(peer_id, 0, &pkt, as_reliable);
        }
+
+       if (!ephemeral)
+               m_playing_sounds[id] = std::move(params);
        return id;
 }
 void Server::stopSound(s32 handle)
 {
-       // Get sound reference
-       std::unordered_map<s32, ServerPlayingSound>::iterator i =
-               m_playing_sounds.find(handle);
-       if (i == m_playing_sounds.end())
+       auto it = m_playing_sounds.find(handle);
+       if (it == m_playing_sounds.end())
                return;
-       ServerPlayingSound &psound = i->second;
+
+       ServerPlayingSound &psound = it->second;
 
        NetworkPacket pkt(TOCLIENT_STOP_SOUND, 4);
        pkt << handle;
 
-       for (std::unordered_set<session_t>::const_iterator si = psound.clients.begin();
-                       si != psound.clients.end(); ++si) {
+       for (session_t peer_id : psound.clients) {
                // Send as reliable
-               m_clients.send(*si, 0, &pkt, true);
+               m_clients.send(peer_id, 0, &pkt, true);
        }
+
        // Remove sound reference
-       m_playing_sounds.erase(i);
+       m_playing_sounds.erase(it);
 }
 
 void Server::fadeSound(s32 handle, float step, float gain)
 {
-       // Get sound reference
-       std::unordered_map<s32, ServerPlayingSound>::iterator i =
-               m_playing_sounds.find(handle);
-       if (i == m_playing_sounds.end())
+       auto it = m_playing_sounds.find(handle);
+       if (it == m_playing_sounds.end())
                return;
 
-       ServerPlayingSound &psound = i->second;
-       psound.params.gain = gain;
+       ServerPlayingSound &psound = it->second;
+       psound.gain = gain; // destination gain
 
        NetworkPacket pkt(TOCLIENT_FADE_SOUND, 4);
        pkt << handle << step << gain;
 
-       // Backwards compability
-       bool play_sound = gain > 0;
-       ServerPlayingSound compat_psound = psound;
-       compat_psound.clients.clear();
-
-       NetworkPacket compat_pkt(TOCLIENT_STOP_SOUND, 4);
-       compat_pkt << handle;
-
-       for (std::unordered_set<u16>::iterator it = psound.clients.begin();
-                       it != psound.clients.end();) {
-               if (m_clients.getProtocolVersion(*it) >= 32) {
-                       // Send as reliable
-                       m_clients.send(*it, 0, &pkt, true);
-                       ++it;
-               } else {
-                       compat_psound.clients.insert(*it);
-                       // Stop old sound
-                       m_clients.send(*it, 0, &compat_pkt, true);
-                       psound.clients.erase(it++);
-               }
+       for (session_t peer_id : psound.clients) {
+               // Send as reliable
+               m_clients.send(peer_id, 0, &pkt, true);
        }
 
        // Remove sound reference
-       if (!play_sound || psound.clients.empty())
-               m_playing_sounds.erase(i);
-
-       if (play_sound && !compat_psound.clients.empty()) {
-               // Play new sound volume on older clients
-               playSound(compat_psound.spec, compat_psound.params);
-       }
+       if (gain <= 0 || psound.clients.empty())
+               m_playing_sounds.erase(it);
 }
 
 void Server::sendRemoveNode(v3s16 p, std::unordered_set<u16> *far_players,
                float far_d_nodes)
 {
-       float maxd = far_d_nodes * BS;
        v3f p_f = intToFloat(p, BS);
        v3s16 block_pos = getNodeBlockPos(p);
 
        NetworkPacket pkt(TOCLIENT_REMOVENODE, 6);
        pkt << p;
 
-       std::vector<session_t> clients = m_clients.getClientIDs();
-       m_clients.lock();
-
-       for (session_t client_id : clients) {
-               RemoteClient *client = m_clients.lockedGetClientNoEx(client_id);
-               if (!client)
-                       continue;
-
-               RemotePlayer *player = m_env->getPlayer(client_id);
-               PlayerSAO *sao = player ? player->getPlayerSAO() : nullptr;
-
-               // If player is far away, only set modified blocks not sent
-               if (!client->isBlockSent(block_pos) || (sao &&
-                               sao->getBasePosition().getDistanceFrom(p_f) > maxd)) {
-                       if (far_players)
-                               far_players->emplace(client_id);
-                       else
-                               client->SetBlockNotSent(block_pos);
-                       continue;
-               }
-
-               // Send as reliable
-               m_clients.send(client_id, 0, &pkt, true);
-       }
-
-       m_clients.unlock();
+       sendNodeChangePkt(pkt, block_pos, p_f, far_d_nodes, far_players);
 }
 
 void Server::sendAddNode(v3s16 p, MapNode n, std::unordered_set<u16> *far_players,
                float far_d_nodes, bool remove_metadata)
 {
-       float maxd = far_d_nodes * BS;
        v3f p_f = intToFloat(p, BS);
        v3s16 block_pos = getNodeBlockPos(p);
 
        NetworkPacket pkt(TOCLIENT_ADDNODE, 6 + 2 + 1 + 1 + 1);
        pkt << p << n.param0 << n.param1 << n.param2
                        << (u8) (remove_metadata ? 0 : 1);
+       sendNodeChangePkt(pkt, block_pos, p_f, far_d_nodes, far_players);
+}
 
+void Server::sendNodeChangePkt(NetworkPacket &pkt, v3s16 block_pos,
+               v3f p, float far_d_nodes, std::unordered_set<u16> *far_players)
+{
+       float maxd = far_d_nodes * BS;
        std::vector<session_t> clients = m_clients.getClientIDs();
-       m_clients.lock();
+       ClientInterface::AutoLock clientlock(m_clients);
 
        for (session_t client_id : clients) {
                RemoteClient *client = m_clients.lockedGetClientNoEx(client_id);
@@ -2234,7 +2292,7 @@ void Server::sendAddNode(v3s16 p, MapNode n, std::unordered_set<u16> *far_player
 
                // If player is far away, only set modified blocks not sent
                if (!client->isBlockSent(block_pos) || (sao &&
-                               sao->getBasePosition().getDistanceFrom(p_f) > maxd)) {
+                               sao->getBasePosition().getDistanceFrom(p) > maxd)) {
                        if (far_players)
                                far_players->emplace(client_id);
                        else
@@ -2245,35 +2303,35 @@ void Server::sendAddNode(v3s16 p, MapNode n, std::unordered_set<u16> *far_player
                // Send as reliable
                m_clients.send(client_id, 0, &pkt, true);
        }
-
-       m_clients.unlock();
 }
 
-void Server::sendMetadataChanged(const std::list<v3s16> &meta_updates, float far_d_nodes)
+void Server::sendMetadataChanged(const std::unordered_set<v3s16> &positions, float far_d_nodes)
 {
-       float maxd = far_d_nodes * BS;
        NodeMetadataList meta_updates_list(false);
-       std::vector<session_t> clients = m_clients.getClientIDs();
+       std::ostringstream os(std::ios::binary);
 
-       m_clients.lock();
+       std::vector<session_t> clients = m_clients.getClientIDs();
+       ClientInterface::AutoLock clientlock(m_clients);
 
        for (session_t i : clients) {
                RemoteClient *client = m_clients.lockedGetClientNoEx(i);
                if (!client)
                        continue;
 
-               ServerActiveObject *player = m_env->getActiveObject(i);
-               v3f player_pos = player ? player->getBasePosition() : v3f();
+               ServerActiveObject *player = getPlayerSAO(i);
+               v3s16 player_pos;
+               if (player)
+                       player_pos = floatToInt(player->getBasePosition(), BS);
 
-               for (const v3s16 &pos : meta_updates) {
+               for (const v3s16 pos : positions) {
                        NodeMetadata *meta = m_env->getMap().getNodeMetadata(pos);
 
                        if (!meta)
                                continue;
 
                        v3s16 block_pos = getNodeBlockPos(pos);
-                       if (!client->isBlockSent(block_pos) || (player &&
-                                       player_pos.getDistanceFrom(intToFloat(pos, BS)) > maxd)) {
+                       if (!client->isBlockSent(block_pos) ||
+                                       player_pos.getDistanceFrom(pos) > far_d_nodes) {
                                client->SetBlockNotSent(block_pos);
                                continue;
                        }
@@ -2285,38 +2343,49 @@ void Server::sendMetadataChanged(const std::list<v3s16> &meta_updates, float far
                        continue;
 
                // Send the meta changes
-               std::ostringstream os(std::ios::binary);
-               meta_updates_list.serialize(os, client->net_proto_version, false, true);
-               std::ostringstream oss(std::ios::binary);
-               compressZlib(os.str(), oss);
-
-               NetworkPacket pkt(TOCLIENT_NODEMETA_CHANGED, 0);
-               pkt.putLongString(oss.str());
-               m_clients.send(i, 0, &pkt, true);
+               os.str("");
+               meta_updates_list.serialize(os, client->serialization_version, false, true, true);
+               std::string raw = os.str();
+               os.str("");
+               compressZlib(raw, os);
+
+               NetworkPacket pkt(TOCLIENT_NODEMETA_CHANGED, 0, i);
+               pkt.putLongString(os.str());
+               Send(&pkt);
 
                meta_updates_list.clear();
        }
-
-       m_clients.unlock();
 }
 
 void Server::SendBlockNoLock(session_t peer_id, MapBlock *block, u8 ver,
-               u16 net_proto_version)
+               u16 net_proto_version, SerializedBlockCache *cache)
 {
-       /*
-               Create a packet with the block in the right format
-       */
+       thread_local const int net_compression_level = rangelim(g_settings->getS16("map_compression_level_net"), -1, 9);
+       std::string s, *sptr = nullptr;
 
-       std::ostringstream os(std::ios_base::binary);
-       block->serialize(os, ver, false);
-       block->serializeNetworkSpecific(os);
-       std::string s = os.str();
+       if (cache) {
+               auto it = cache->find({block->getPos(), ver});
+               if (it != cache->end())
+                       sptr = &it->second;
+       }
 
-       NetworkPacket pkt(TOCLIENT_BLOCKDATA, 2 + 2 + 2 + 2 + s.size(), peer_id);
+       // Serialize the block in the right format
+       if (!sptr) {
+               std::ostringstream os(std::ios_base::binary);
+               block->serialize(os, ver, false, net_compression_level);
+               block->serializeNetworkSpecific(os);
+               s = os.str();
+               sptr = &s;
+       }
 
+       NetworkPacket pkt(TOCLIENT_BLOCKDATA, 2 + 2 + 2 + sptr->size(), peer_id);
        pkt << block->getPos();
-       pkt.putRawString(s.c_str(), s.size());
+       pkt.putRawString(*sptr);
        Send(&pkt);
+
+       // Store away in cache
+       if (cache && sptr == &s)
+               (*cache)[{block->getPos(), ver}] = std::move(s);
 }
 
 void Server::SendBlocks(float dtime)
@@ -2326,14 +2395,14 @@ void Server::SendBlocks(float dtime)
 
        std::vector<PrioritySortedBlockTransfer> queue;
 
-       u32 total_sending = 0;
+       u32 total_sending = 0, unique_clients = 0;
 
        {
                ScopeProfiler sp2(g_profiler, "Server::SendBlocks(): Collect list");
 
                std::vector<session_t> clients = m_clients.getClientIDs();
 
-               m_clients.lock();
+               ClientInterface::AutoLock clientlock(m_clients);
                for (const session_t client_id : clients) {
                        RemoteClient *client = m_clients.lockedGetClientNoEx(client_id, CS_Active);
 
@@ -2341,9 +2410,10 @@ void Server::SendBlocks(float dtime)
                                continue;
 
                        total_sending += client->getSendingCount();
+                       const auto old_count = queue.size();
                        client->GetNextBlocks(m_env,m_emerge, dtime, queue);
+                       unique_clients += queue.size() > old_count ? 1 : 0;
                }
-               m_clients.unlock();
        }
 
        // Sort.
@@ -2351,7 +2421,7 @@ void Server::SendBlocks(float dtime)
        // Lowest is most important.
        std::sort(queue.begin(), queue.end());
 
-       m_clients.lock();
+       ClientInterface::AutoLock clientlock(m_clients);
 
        // Maximal total count calculation
        // The per-client block sends is halved with the maximal online users
@@ -2361,6 +2431,12 @@ void Server::SendBlocks(float dtime)
        ScopeProfiler sp(g_profiler, "Server::SendBlocks(): Send to clients");
        Map &map = m_env->getMap();
 
+       SerializedBlockCache cache, *cache_ptr = nullptr;
+       if (unique_clients > 1) {
+               // caching is pointless with a single client
+               cache_ptr = &cache;
+       }
+
        for (const PrioritySortedBlockTransfer &block_to_send : queue) {
                if (total_sending >= max_blocks_to_send)
                        break;
@@ -2375,12 +2451,11 @@ void Server::SendBlocks(float dtime)
                        continue;
 
                SendBlockNoLock(block_to_send.peer_id, block, client->serialization_version,
-                               client->net_proto_version);
+                               client->net_proto_version, cache_ptr);
 
                client->SentBlock(block_to_send.pos);
                total_sending++;
        }
-       m_clients.unlock();
 }
 
 bool Server::SendBlock(session_t peer_id, const v3s16 &blockpos)
@@ -2389,114 +2464,111 @@ bool Server::SendBlock(session_t peer_id, const v3s16 &blockpos)
        if (!block)
                return false;
 
-       m_clients.lock();
+       ClientInterface::AutoLock clientlock(m_clients);
        RemoteClient *client = m_clients.lockedGetClientNoEx(peer_id, CS_Active);
-       if (!client || client->isBlockSent(blockpos)) {
-               m_clients.unlock();
+       if (!client || client->isBlockSent(blockpos))
                return false;
-       }
        SendBlockNoLock(peer_id, block, client->serialization_version,
                        client->net_proto_version);
-       m_clients.unlock();
 
        return true;
 }
 
+bool Server::addMediaFile(const std::string &filename,
+       const std::string &filepath, std::string *filedata_to,
+       std::string *digest_to)
+{
+       // If name contains illegal characters, ignore the file
+       if (!string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) {
+               infostream << "Server: ignoring illegal file name: \""
+                               << filename << "\"" << std::endl;
+               return false;
+       }
+       // If name is not in a supported format, ignore it
+       const char *supported_ext[] = {
+               ".png", ".jpg", ".bmp", ".tga",
+               ".ogg",
+               ".x", ".b3d", ".obj",
+               // Custom translation file format
+               ".tr",
+               NULL
+       };
+       if (removeStringEnd(filename, supported_ext).empty()) {
+               infostream << "Server: ignoring unsupported file extension: \""
+                               << filename << "\"" << std::endl;
+               return false;
+       }
+       // Ok, attempt to load the file and add to cache
+
+       // Read data
+       std::string filedata;
+       if (!fs::ReadFile(filepath, filedata)) {
+               errorstream << "Server::addMediaFile(): Failed to open \""
+                                       << filename << "\" for reading" << std::endl;
+               return false;
+       }
+
+       if (filedata.empty()) {
+               errorstream << "Server::addMediaFile(): Empty file \""
+                               << filepath << "\"" << std::endl;
+               return false;
+       }
+
+       SHA1 sha1;
+       sha1.addBytes(filedata.c_str(), filedata.length());
+
+       unsigned char *digest = sha1.getDigest();
+       std::string sha1_base64 = base64_encode(digest, 20);
+       std::string sha1_hex = hex_encode((char*) digest, 20);
+       if (digest_to)
+               *digest_to = std::string((char*) digest, 20);
+       free(digest);
+
+       // Put in list
+       m_media[filename] = MediaInfo(filepath, sha1_base64);
+       verbosestream << "Server: " << sha1_hex << " is " << filename
+                       << std::endl;
+
+       if (filedata_to)
+               *filedata_to = std::move(filedata);
+       return true;
+}
+
 void Server::fillMediaCache()
 {
-       infostream<<"Server: Calculating media file checksums"<<std::endl;
+       infostream << "Server: Calculating media file checksums" << std::endl;
 
        // Collect all media file paths
        std::vector<std::string> paths;
-       m_modmgr->getModsMediaPaths(paths);
-       fs::GetRecursiveDirs(paths, m_gamespec.path + DIR_DELIM + "textures");
+
+       // ordered in descending priority
+       paths.push_back(getBuiltinLuaPath() + DIR_DELIM + "locale");
        fs::GetRecursiveDirs(paths, porting::path_user + DIR_DELIM + "textures" + DIR_DELIM + "server");
+       fs::GetRecursiveDirs(paths, m_gamespec.path + DIR_DELIM + "textures");
+       m_modmgr->getModsMediaPaths(paths);
 
        // Collect media file information from paths into cache
        for (const std::string &mediapath : paths) {
                std::vector<fs::DirListNode> dirlist = fs::GetDirListing(mediapath);
                for (const fs::DirListNode &dln : dirlist) {
-                       if (dln.dir) // Ignode dirs
-                               continue;
-                       std::string filename = dln.name;
-                       // If name contains illegal characters, ignore the file
-                       if (!string_allowed(filename, TEXTURENAME_ALLOWED_CHARS)) {
-                               infostream<<"Server: ignoring illegal file name: \""
-                                               << filename << "\"" << std::endl;
-                               continue;
-                       }
-                       // If name is not in a supported format, ignore it
-                       const char *supported_ext[] = {
-                               ".png", ".jpg", ".bmp", ".tga",
-                               ".pcx", ".ppm", ".psd", ".wal", ".rgb",
-                               ".ogg",
-                               ".x", ".b3d", ".md2", ".obj",
-                               // Custom translation file format
-                               ".tr",
-                               NULL
-                       };
-                       if (removeStringEnd(filename, supported_ext).empty()){
-                               infostream << "Server: ignoring unsupported file extension: \""
-                                               << filename << "\"" << std::endl;
-                               continue;
-                       }
-                       // Ok, attempt to load the file and add to cache
-                       std::string filepath;
-                       filepath.append(mediapath).append(DIR_DELIM).append(filename);
-
-                       // Read data
-                       std::ifstream fis(filepath.c_str(), std::ios_base::binary);
-                       if (!fis.good()) {
-                               errorstream << "Server::fillMediaCache(): Could not open \""
-                                               << filename << "\" for reading" << std::endl;
+                       if (dln.dir) // Ignore dirs (already in paths)
                                continue;
-                       }
-                       std::ostringstream tmp_os(std::ios_base::binary);
-                       bool bad = false;
-                       for(;;) {
-                               char buf[1024];
-                               fis.read(buf, 1024);
-                               std::streamsize len = fis.gcount();
-                               tmp_os.write(buf, len);
-                               if (fis.eof())
-                                       break;
-                               if (!fis.good()) {
-                                       bad = true;
-                                       break;
-                               }
-                       }
-                       if(bad) {
-                               errorstream<<"Server::fillMediaCache(): Failed to read \""
-                                               << filename << "\"" << std::endl;
-                               continue;
-                       }
-                       if(tmp_os.str().length() == 0) {
-                               errorstream << "Server::fillMediaCache(): Empty file \""
-                                               << filepath << "\"" << std::endl;
-                               continue;
-                       }
-
-                       SHA1 sha1;
-                       sha1.addBytes(tmp_os.str().c_str(), tmp_os.str().length());
 
-                       unsigned char *digest = sha1.getDigest();
-                       std::string sha1_base64 = base64_encode(digest, 20);
-                       std::string sha1_hex = hex_encode((char*)digest, 20);
-                       free(digest);
+                       const std::string &filename = dln.name;
+                       if (m_media.find(filename) != m_media.end()) // Do not override
+                               continue;
 
-                       // Put in list
-                       m_media[filename] = MediaInfo(filepath, sha1_base64);
-                       verbosestream << "Server: " << sha1_hex << " is " << filename
-                                       << std::endl;
+                       std::string filepath = mediapath;
+                       filepath.append(DIR_DELIM).append(filename);
+                       addMediaFile(filename, filepath);
                }
        }
+
+       infostream << "Server: " << m_media.size() << " media files collected" << std::endl;
 }
 
 void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_code)
 {
-       verbosestream << "Server: Announcing files to id(" << peer_id << ")"
-               << std::endl;
-
        // Make packet
        NetworkPacket pkt(TOCLIENT_ANNOUNCE_MEDIA, 0, peer_id);
 
@@ -2504,6 +2576,8 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co
        std::string lang_suffix;
        lang_suffix.append(".").append(lang_code).append(".tr");
        for (const auto &i : m_media) {
+               if (i.second.no_announce)
+                       continue;
                if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
                        continue;
                media_sent++;
@@ -2512,6 +2586,8 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co
        pkt << media_sent;
 
        for (const auto &i : m_media) {
+               if (i.second.no_announce)
+                       continue;
                if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
                        continue;
                pkt << i.first << i.second.sha1_digest;
@@ -2519,6 +2595,9 @@ void Server::sendMediaAnnouncement(session_t peer_id, const std::string &lang_co
 
        pkt << g_settings->get("remote_media");
        Send(&pkt);
+
+       verbosestream << "Server: Announcing files to id(" << peer_id
+               << "): count=" << media_sent << " size=" << pkt.getSize() << std::endl;
 }
 
 struct SendableMedia
@@ -2527,11 +2606,9 @@ struct SendableMedia
        std::string path;
        std::string data;
 
-       SendableMedia(const std::string &name_="", const std::string &path_="",
-                     const std::string &data_=""):
-               name(name_),
-               path(path_),
-               data(data_)
+       SendableMedia(const std::string &name, const std::string &path,
+                       std::string &&data):
+               name(name), path(path), data(std::move(data))
        {}
 };
 
@@ -2558,40 +2635,19 @@ void Server::sendRequestedMedia(session_t peer_id,
                        continue;
                }
 
-               //TODO get path + name
-               std::string tpath = m_media[name].path;
+               const auto &m = m_media[name];
 
                // Read data
-               std::ifstream fis(tpath.c_str(), std::ios_base::binary);
-               if(!fis.good()){
-                       errorstream<<"Server::sendRequestedMedia(): Could not open \""
-                                       <<tpath<<"\" for reading"<<std::endl;
+               std::string data;
+               if (!fs::ReadFile(m.path, data)) {
+                       errorstream << "Server::sendRequestedMedia(): Failed to read \""
+                                       << name << "\"" << std::endl;
                        continue;
                }
-               std::ostringstream tmp_os(std::ios_base::binary);
-               bool bad = false;
-               for(;;) {
-                       char buf[1024];
-                       fis.read(buf, 1024);
-                       std::streamsize len = fis.gcount();
-                       tmp_os.write(buf, len);
-                       file_size_bunch_total += len;
-                       if(fis.eof())
-                               break;
-                       if(!fis.good()) {
-                               bad = true;
-                               break;
-                       }
-               }
-               if (bad) {
-                       errorstream<<"Server::sendRequestedMedia(): Failed to read \""
-                                       <<name<<"\""<<std::endl;
-                       continue;
-               }
-               /*infostream<<"Server::sendRequestedMedia(): Loaded \""
-                               <<tname<<"\""<<std::endl;*/
+               file_size_bunch_total += data.size();
+
                // Put in list
-               file_bunches[file_bunches.size()-1].emplace_back(name, tpath, tmp_os.str());
+               file_bunches.back().emplace_back(name, m.path, std::move(data));
 
                // Start next bunch if got enough data
                if(file_size_bunch_total >= bytes_per_bunch) {
@@ -2634,40 +2690,64 @@ void Server::sendRequestedMedia(session_t peer_id,
        }
 }
 
-void Server::sendDetachedInventory(const std::string &name, session_t peer_id)
+void Server::stepPendingDynMediaCallbacks(float dtime)
 {
-       const auto &inv_it = m_detached_inventories.find(name);
-       const auto &player_it = m_detached_inventories_player.find(name);
+       MutexAutoLock lock(m_env_mutex);
 
-       if (player_it == m_detached_inventories_player.end() ||
-                       player_it->second.empty()) {
-               // OK. Send to everyone
-       } else {
-               if (!m_env)
-                       return; // Mods are not done loading
+       for (auto it = m_pending_dyn_media.begin(); it != m_pending_dyn_media.end();) {
+               it->second.expiry_timer -= dtime;
+               bool del = it->second.waiting_players.empty() || it->second.expiry_timer < 0;
 
-               RemotePlayer *p = m_env->getPlayer(player_it->second.c_str());
-               if (!p)
-                       return; // Player is offline
+               if (!del) {
+                       it++;
+                       continue;
+               }
 
-               if (peer_id != PEER_ID_INEXISTENT && peer_id != p->getPeerId())
-                       return; // Caller requested send to a different player, so don't send.
+               const auto &name = it->second.filename;
+               if (!name.empty()) {
+                       assert(m_media.count(name));
+                       // if no_announce isn't set we're definitely deleting the wrong file!
+                       sanity_check(m_media[name].no_announce);
 
-               peer_id = p->getPeerId();
+                       fs::DeleteSingleFileOrEmptyDirectory(m_media[name].path);
+                       m_media.erase(name);
+               }
+               getScriptIface()->freeDynamicMediaCallback(it->first);
+               it = m_pending_dyn_media.erase(it);
        }
+}
 
+void Server::SendMinimapModes(session_t peer_id,
+               std::vector<MinimapMode> &modes, size_t wanted_mode)
+{
+       RemotePlayer *player = m_env->getPlayer(peer_id);
+       assert(player);
+       if (player->getPeerId() == PEER_ID_INEXISTENT)
+               return;
+
+       NetworkPacket pkt(TOCLIENT_MINIMAP_MODES, 0, peer_id);
+       pkt << (u16)modes.size() << (u16)wanted_mode;
+
+       for (auto &mode : modes)
+               pkt << (u16)mode.type << mode.label << mode.size << mode.texture << mode.scale;
+
+       Send(&pkt);
+}
+
+void Server::sendDetachedInventory(Inventory *inventory, const std::string &name, session_t peer_id)
+{
        NetworkPacket pkt(TOCLIENT_DETACHED_INVENTORY, 0, peer_id);
        pkt << name;
 
-       if (inv_it == m_detached_inventories.end()) {
+       if (!inventory) {
                pkt << false; // Remove inventory
        } else {
                pkt << true; // Update inventory
 
                // Serialization & NetworkPacket isn't a love story
                std::ostringstream os(std::ios_base::binary);
-               inv_it->second->serialize(os);
-               inv_it->second->setModified(false);
+               inventory->serialize(os);
+               inventory->setModified(false);
 
                const std::string &os_str = os.str();
                pkt << static_cast<u16>(os_str.size()); // HACK: to keep compatibility with 5.0.0 clients
@@ -2682,42 +2762,35 @@ void Server::sendDetachedInventory(const std::string &name, session_t peer_id)
 
 void Server::sendDetachedInventories(session_t peer_id, bool incremental)
 {
-       for (const auto &detached_inventory : m_detached_inventories) {
-               const std::string &name = detached_inventory.first;
-               if (incremental) {
-                       Inventory *inv = detached_inventory.second;
-                       if (!inv || !inv->checkModified())
-                               continue;
-               }
-
-               sendDetachedInventory(name, peer_id);
+       // Lookup player name, to filter detached inventories just after
+       std::string peer_name;
+       if (peer_id != PEER_ID_INEXISTENT) {
+               peer_name = getClient(peer_id, CS_Created)->getName();
        }
+
+       auto send_cb = [this, peer_id](const std::string &name, Inventory *inv) {
+               sendDetachedInventory(inv, name, peer_id);
+       };
+
+       m_inventory_mgr->sendDetachedInventories(peer_name, incremental, send_cb);
 }
 
 /*
        Something random
 */
 
-void Server::DiePlayer(session_t peer_id, const PlayerHPChangeReason &reason)
+void Server::HandlePlayerDeath(PlayerSAO *playersao, const PlayerHPChangeReason &reason)
 {
-       PlayerSAO *playersao = getPlayerSAO(peer_id);
-       // In some rare cases this can be NULL -- if the player is disconnected
-       // when a Lua function modifies l_punch, for example
-       if (!playersao)
-               return;
-
        infostream << "Server::DiePlayer(): Player "
                        << playersao->getPlayer()->getName()
                        << " dies" << std::endl;
 
-       playersao->setHP(0, reason);
        playersao->clearParentAttachment();
 
        // Trigger scripted stuff
        m_script->on_dieplayer(playersao, reason);
 
-       SendPlayerHP(peer_id);
-       SendDeathscreen(peer_id, false, v3f(0,0,0));
+       SendDeathscreen(playersao->getPeerID(), false, v3f(0,0,0));
 }
 
 void Server::RespawnPlayer(session_t peer_id)
@@ -2729,17 +2802,16 @@ void Server::RespawnPlayer(session_t peer_id)
                        << playersao->getPlayer()->getName()
                        << " respawns" << std::endl;
 
-       playersao->setHP(playersao->accessObjectProperties()->hp_max,
+       const auto *prop = playersao->accessObjectProperties();
+       playersao->setHP(prop->hp_max,
                        PlayerHPChangeReason(PlayerHPChangeReason::RESPAWN));
-       playersao->setBreath(playersao->accessObjectProperties()->breath_max);
+       playersao->setBreath(prop->breath_max);
 
        bool repositioned = m_script->on_respawnplayer(playersao);
        if (!repositioned) {
                // setPos will send the new position to client
                playersao->setPos(findSpawnPos());
        }
-
-       SendPlayerHP(peer_id);
 }
 
 
@@ -2750,29 +2822,10 @@ void Server::DenySudoAccess(session_t peer_id)
 }
 
 
-void Server::DenyAccessVerCompliant(session_t peer_id, u16 proto_ver, AccessDeniedCode reason,
-               const std::string &str_reason, bool reconnect)
-{
-       SendAccessDenied(peer_id, reason, str_reason, reconnect);
-
-       m_clients.event(peer_id, CSE_SetDenied);
-       DisconnectPeer(peer_id);
-}
-
-
 void Server::DenyAccess(session_t peer_id, AccessDeniedCode reason,
-               const std::string &custom_reason)
-{
-       SendAccessDenied(peer_id, reason, custom_reason);
-       m_clients.event(peer_id, CSE_SetDenied);
-       DisconnectPeer(peer_id);
-}
-
-// 13/03/15: remove this function when protocol version 25 will become
-// the minimum version for MT users, maybe in 1 year
-void Server::DenyAccess_Legacy(session_t peer_id, const std::wstring &reason)
+               const std::string &custom_reason, bool reconnect)
 {
-       SendAccessDenied_Legacy(peer_id, reason);
+       SendAccessDenied(peer_id, reason, custom_reason, reconnect);
        m_clients.event(peer_id, CSE_SetDenied);
        DisconnectPeer(peer_id);
 }
@@ -2900,10 +2953,8 @@ void Server::UpdateCrafting(RemotePlayer *player)
        if (!clist || clist->getSize() == 0)
                return;
 
-       if (!clist->checkModified()) {
-               verbosestream << "Skip Server::UpdateCrafting(): list unmodified" << std::endl;
+       if (!clist->checkModified())
                return;
-       }
 
        // Get a preview for crafting
        ItemStack preview;
@@ -2940,7 +2991,7 @@ void Server::handleChatInterfaceEvent(ChatEvent *evt)
        }
 }
 
-std::wstring Server::handleChat(const std::string &name, const std::wstring &wname,
+std::wstring Server::handleChat(const std::string &name,
        std::wstring wmessage, bool check_shout_priv, RemotePlayer *player)
 {
        // If something goes wrong, this player is to blame
@@ -2960,8 +3011,8 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
                        return ws.str();
                }
                case RPLAYER_CHATRESULT_KICK:
-                       DenyAccess_Legacy(player->getPeerId(),
-                                       L"You have been kicked due to message flooding.");
+                       DenyAccess(player->getPeerId(), SERVER_ACCESSDENIED_CUSTOM_STRING,
+                               "You have been kicked due to message flooding.");
                        return L"";
                case RPLAYER_CHATRESULT_OK:
                        break;
@@ -2977,8 +3028,11 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
        }
 
        auto message = trim(wide_to_utf8(wmessage));
+       if (message.empty())
+               return L"";
+
        if (message.find_first_of("\n\r") != std::wstring::npos) {
-               return L"New lines are not permitted in chat messages";
+               return L"Newlines are not permitted in chat messages";
        }
 
        // Run script hook, exit if script ate the chat message
@@ -2994,8 +3048,16 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
                line += L"-!- You don't have permission to shout.";
                broadcast_line = false;
        } else {
-               line += narrow_to_wide(m_script->formatChatMessage(name,
-                               wide_to_narrow(wmessage)));
+               /*
+                       Workaround for fixing chat on Android. Lua doesn't handle
+                       the Cyrillic alphabet and some characters on older Android devices
+               */
+#ifdef __ANDROID__
+               line += L"<" + utf8_to_wide(name) + L"> " + wmessage;
+#else
+               line += utf8_to_wide(m_script->formatChatMessage(name,
+                               wide_to_utf8(wmessage)));
+#endif
        }
 
        /*
@@ -3007,35 +3069,23 @@ std::wstring Server::handleChat(const std::string &name, const std::wstring &wna
        /*
                Send the message to others
        */
-       actionstream << "CHAT: " << wide_to_narrow(unescape_enriched(line)) << std::endl;
-
-       std::vector<session_t> clients = m_clients.getClientIDs();
-
-       /*
-               Send the message back to the inital sender
-               if they are using protocol version >= 29
-       */
+       actionstream << "CHAT: " << wide_to_utf8(unescape_enriched(line)) << std::endl;
 
-       session_t peer_id_to_avoid_sending =
-               (player ? player->getPeerId() : PEER_ID_INEXISTENT);
+       ChatMessage chatmsg(line);
 
-       if (player && player->protocol_version >= 29)
-               peer_id_to_avoid_sending = PEER_ID_INEXISTENT;
+       std::vector<session_t> clients = m_clients.getClientIDs();
+       for (u16 cid : clients)
+               SendChatMessage(cid, chatmsg);
 
-       for (u16 cid : clients) {
-               if (cid != peer_id_to_avoid_sending)
-                       SendChatMessage(cid, ChatMessage(line));
-       }
        return L"";
 }
 
 void Server::handleAdminChat(const ChatEventChat *evt)
 {
        std::string name = evt->nick;
-       std::wstring wname = utf8_to_wide(name);
        std::wstring wmessage = evt->evt_msg;
 
-       std::wstring answer = handleChat(name, wname, wmessage);
+       std::wstring answer = handleChat(name, wmessage);
 
        // If asked to send answer to sender
        if (!answer.empty()) {
@@ -3072,46 +3122,45 @@ PlayerSAO *Server::getPlayerSAO(session_t peer_id)
        return player->getPlayerSAO();
 }
 
-std::wstring Server::getStatusString()
+std::string Server::getStatusString()
 {
-       std::wostringstream os(std::ios_base::binary);
-       os << L"# Server: ";
+       std::ostringstream os(std::ios_base::binary);
+       os << "# Server: ";
        // Version
-       os << L"version=" << narrow_to_wide(g_version_string);
+       os << "version: " << g_version_string;
+       // Game
+       os << " | game: " << (m_gamespec.title.empty() ? m_gamespec.id : m_gamespec.title);
        // Uptime
-       os << L", uptime=" << m_uptime.get();
+       os << " | uptime: " << duration_to_string((int) m_uptime_counter->get());
        // Max lag estimate
-       os << L", max_lag=" << (m_env ? m_env->getMaxLagEstimate() : 0);
+       os << " | max lag: " << std::setprecision(3);
+       os << (m_env ? m_env->getMaxLagEstimate() : 0) << "s";
 
        // Information about clients
        bool first = true;
-       os << L", clients={";
+       os << " | clients: ";
        if (m_env) {
                std::vector<session_t> clients = m_clients.getClientIDs();
                for (session_t client_id : clients) {
                        RemotePlayer *player = m_env->getPlayer(client_id);
 
                        // Get name of player
-                       std::wstring name = L"unknown";
-                       if (player)
-                               name = narrow_to_wide(player->getName());
+                       const char *name = player ? player->getName() : "<unknown>";
 
                        // Add name to information string
                        if (!first)
-                               os << L", ";
+                               os << ", ";
                        else
                                first = false;
-
                        os << name;
                }
        }
-       os << L"}";
 
        if (m_env && !((ServerMap*)(&m_env->getMap()))->isSavingEnabled())
-               os << std::endl << L"# Server: " << " WARNING: Map saving is disabled.";
+               os << std::endl << "# Server: " << " WARNING: Map saving is disabled.";
 
        if (!g_settings->get("motd").empty())
-               os << std::endl << L"# Server: " << narrow_to_wide(g_settings->get("motd"));
+               os << std::endl << "# Server: " << g_settings->get("motd");
 
        return os.str();
 }
@@ -3259,9 +3308,12 @@ bool Server::hudSetFlags(RemotePlayer *player, u32 flags, u32 mask)
        if (!player)
                return false;
 
+       u32 new_hud_flags = (player->hud_flags & ~mask) | flags;
+       if (new_hud_flags == player->hud_flags) // no change
+               return true;
+
        SendHUDSetFlags(player->getPeerId(), flags, mask);
-       player->hud_flags &= ~mask;
-       player->hud_flags |= flags;
+       player->hud_flags = new_hud_flags;
 
        PlayerSAO* playersao = player->getPlayerSAO();
 
@@ -3307,7 +3359,8 @@ void Server::hudSetHotbarSelectedImage(RemotePlayer *player, const std::string &
 
 Address Server::getPeerAddress(session_t peer_id)
 {
-       return m_con->GetPeerAddress(peer_id);
+       // Note that this is only set after Init was received in Server::handleCommand_Init
+       return getClient(peer_id, CS_Invalid)->getAddress();
 }
 
 void Server::setLocalPlayerAnimations(RemotePlayer *player,
@@ -3326,13 +3379,32 @@ void Server::setPlayerEyeOffset(RemotePlayer *player, const v3f &first, const v3
        SendEyeOffset(player->getPeerId(), first, third);
 }
 
-void Server::setSky(RemotePlayer *player, const video::SColor &bgcolor,
-       const std::string &type, const std::vector<std::string> &params,
-       bool &clouds)
+void Server::setSky(RemotePlayer *player, const SkyboxParams &params)
+{
+       sanity_check(player);
+       player->setSky(params);
+       SendSetSky(player->getPeerId(), params);
+}
+
+void Server::setSun(RemotePlayer *player, const SunParams &params)
 {
        sanity_check(player);
-       player->setSky(bgcolor, type, params, clouds);
-       SendSetSky(player->getPeerId(), bgcolor, type, params, clouds);
+       player->setSun(params);
+       SendSetSun(player->getPeerId(), params);
+}
+
+void Server::setMoon(RemotePlayer *player, const MoonParams &params)
+{
+       sanity_check(player);
+       player->setMoon(params);
+       SendSetMoon(player->getPeerId(), params);
+}
+
+void Server::setStars(RemotePlayer *player, const StarParams &params)
+{
+       sanity_check(player);
+       player->setStars(params);
+       SendSetStars(player->getPeerId(), params);
 }
 
 void Server::setClouds(RemotePlayer *player, const CloudParams &params)
@@ -3342,15 +3414,19 @@ void Server::setClouds(RemotePlayer *player, const CloudParams &params)
        SendCloudParams(player->getPeerId(), params);
 }
 
-bool Server::overrideDayNightRatio(RemotePlayer *player, bool do_override,
+void Server::overrideDayNightRatio(RemotePlayer *player, bool do_override,
        float ratio)
 {
-       if (!player)
-               return false;
-
+       sanity_check(player);
        player->overrideDayNightRatio(do_override, ratio);
        SendOverrideDayNightRatio(player->getPeerId(), do_override, ratio);
-       return true;
+}
+
+void Server::setLighting(RemotePlayer *player, const Lighting &lighting)
+{
+       sanity_check(player);
+       player->setLighting(lighting);
+       SendSetLighting(player->getPeerId(), lighting);
 }
 
 void Server::notifyPlayers(const std::wstring &msg)
@@ -3358,12 +3434,8 @@ void Server::notifyPlayers(const std::wstring &msg)
        SendChatMessage(PEER_ID_INEXISTENT, ChatMessage(msg));
 }
 
-void Server::spawnParticle(const std::string &playername, v3f pos,
-       v3f velocity, v3f acceleration,
-       float expirationtime, float size, bool
-       collisiondetection, bool collision_removal, bool object_collision,
-       bool vertical, const std::string &texture,
-       const struct TileAnimationParams &animation, u8 glow)
+void Server::spawnParticle(const std::string &playername,
+       const ParticleParameters &p)
 {
        // m_env will be NULL if the server is initializing
        if (!m_env)
@@ -3379,18 +3451,11 @@ void Server::spawnParticle(const std::string &playername, v3f pos,
                proto_ver = player->protocol_version;
        }
 
-       SendSpawnParticle(peer_id, proto_ver, pos, velocity, acceleration,
-                       expirationtime, size, collisiondetection, collision_removal,
-                       object_collision, vertical, texture, animation, glow);
+       SendSpawnParticle(peer_id, proto_ver, p);
 }
 
-u32 Server::addParticleSpawner(u16 amount, float spawntime,
-       v3f minpos, v3f maxpos, v3f minvel, v3f maxvel, v3f minacc, v3f maxacc,
-       float minexptime, float maxexptime, float minsize, float maxsize,
-       bool collisiondetection, bool collision_removal, bool object_collision,
-       ServerActiveObject *attached, bool vertical, const std::string &texture,
-       const std::string &playername, const struct TileAnimationParams &animation,
-       u8 glow)
+u32 Server::addParticleSpawner(const ParticleSpawnerParameters &p,
+       ServerActiveObject *attached, const std::string &playername)
 {
        // m_env will be NULL if the server is initializing
        if (!m_env)
@@ -3410,16 +3475,11 @@ u32 Server::addParticleSpawner(u16 amount, float spawntime,
 
        u32 id;
        if (attached_id == 0)
-               id = m_env->addParticleSpawner(spawntime);
+               id = m_env->addParticleSpawner(p.time);
        else
-               id = m_env->addParticleSpawner(spawntime, attached_id);
-
-       SendAddParticleSpawner(peer_id, proto_ver, amount, spawntime,
-               minpos, maxpos, minvel, maxvel, minacc, maxacc,
-               minexptime, maxexptime, minsize, maxsize, collisiondetection,
-               collision_removal, object_collision, attached_id, vertical,
-               texture, id, animation, glow);
+               id = m_env->addParticleSpawner(p.time, attached_id);
 
+       SendAddParticleSpawner(peer_id, proto_ver, p, attached_id, id);
        return id;
 }
 
@@ -3441,49 +3501,126 @@ void Server::deleteParticleSpawner(const std::string &playername, u32 id)
        SendDeleteParticleSpawner(peer_id, id);
 }
 
-Inventory* Server::createDetachedInventory(const std::string &name, const std::string &player)
+bool Server::dynamicAddMedia(std::string filepath,
+       const u32 token, const std::string &to_player, bool ephemeral)
 {
-       if(m_detached_inventories.count(name) > 0){
-               infostream<<"Server clearing detached inventory \""<<name<<"\""<<std::endl;
-               delete m_detached_inventories[name];
-       } else {
-               infostream<<"Server creating detached inventory \""<<name<<"\""<<std::endl;
+       std::string filename = fs::GetFilenameFromPath(filepath.c_str());
+       auto it = m_media.find(filename);
+       if (it != m_media.end()) {
+               // Allow the same path to be "added" again in certain conditions
+               if (ephemeral || it->second.path != filepath) {
+                       errorstream << "Server::dynamicAddMedia(): file \"" << filename
+                               << "\" already exists in media cache" << std::endl;
+                       return false;
+               }
        }
-       Inventory *inv = new Inventory(m_itemdef);
-       sanity_check(inv);
-       m_detached_inventories[name] = inv;
-       if (!player.empty())
-               m_detached_inventories_player[name] = player;
-
-       //TODO find a better way to do this
-       sendDetachedInventory(name,PEER_ID_INEXISTENT);
-       return inv;
-}
 
-bool Server::removeDetachedInventory(const std::string &name)
-{
-       const auto &inv_it = m_detached_inventories.find(name);
-       if (inv_it == m_detached_inventories.end())
+       // Load the file and add it to our media cache
+       std::string filedata, raw_hash;
+       bool ok = addMediaFile(filename, filepath, &filedata, &raw_hash);
+       if (!ok)
                return false;
 
-       delete inv_it->second;
-       m_detached_inventories.erase(inv_it);
+       if (ephemeral) {
+               // Create a copy of the file and swap out the path, this removes the
+               // requirement that mods keep the file accessible at the original path.
+               filepath = fs::CreateTempFile();
+               bool ok = ([&] () -> bool {
+                       if (filepath.empty())
+                               return false;
+                       std::ofstream os(filepath.c_str(), std::ios::binary);
+                       if (!os.good())
+                               return false;
+                       os << filedata;
+                       os.close();
+                       return !os.fail();
+               })();
+               if (!ok) {
+                       errorstream << "Server: failed to create a copy of media file "
+                               << "\"" << filename << "\"" << std::endl;
+                       m_media.erase(filename);
+                       return false;
+               }
+               verbosestream << "Server: \"" << filename << "\" temporarily copied to "
+                       << filepath << std::endl;
+
+               m_media[filename].path = filepath;
+               m_media[filename].no_announce = true;
+               // stepPendingDynMediaCallbacks will clean this up later.
+       } else if (!to_player.empty()) {
+               m_media[filename].no_announce = true;
+       }
 
-       if (!m_env) // Mods are not done loading
-               return true;
+       // Push file to existing clients
+       NetworkPacket pkt(TOCLIENT_MEDIA_PUSH, 0);
+       pkt << raw_hash << filename << (bool)ephemeral;
 
-       const auto &player_it = m_detached_inventories_player.find(name);
-       if (player_it != m_detached_inventories_player.end()) {
-               RemotePlayer *player = m_env->getPlayer(player_it->second.c_str());
+       NetworkPacket legacy_pkt = pkt;
 
-               if (player && player->getPeerId() != PEER_ID_INEXISTENT)
-                       sendDetachedInventory(name, player->getPeerId());
+       // Newer clients get asked to fetch the file (asynchronous)
+       pkt << token;
+       // Older clients have an awful hack that just throws the data at them
+       legacy_pkt.putLongString(filedata);
 
-               m_detached_inventories_player.erase(player_it);
-       } else {
-               // Notify all players about the change
-               sendDetachedInventory(name, PEER_ID_INEXISTENT);
+       std::unordered_set<session_t> delivered, waiting;
+       {
+               ClientInterface::AutoLock clientlock(m_clients);
+               for (auto &pair : m_clients.getClientList()) {
+                       if (pair.second->getState() == CS_DefinitionsSent && !ephemeral) {
+                               /*
+                                       If a client is in the DefinitionsSent state it is too late to
+                                       transfer the file via sendMediaAnnouncement() but at the same
+                                       time the client cannot accept a media push yet.
+                                       Short of artificially delaying the joining process there is no
+                                       way for the server to resolve this so we (currently) opt not to.
+                               */
+                               warningstream << "The media \"" << filename << "\" (dynamic) could "
+                                       "not be delivered to " << pair.second->getName()
+                                       << " due to a race condition." << std::endl;
+                               continue;
+                       }
+                       if (pair.second->getState() < CS_Active)
+                               continue;
+
+                       const auto proto_ver = pair.second->net_proto_version;
+                       if (proto_ver < 39)
+                               continue;
+
+                       const session_t peer_id = pair.second->peer_id;
+                       if (!to_player.empty() && getPlayerName(peer_id) != to_player)
+                               continue;
+
+                       if (proto_ver < 40) {
+                               delivered.emplace(peer_id);
+                               /*
+                                       The network layer only guarantees ordered delivery inside a channel.
+                                       Since the very next packet could be one that uses the media, we have
+                                       to push the media over ALL channels to ensure it is processed before
+                                       it is used. In practice this means channels 1 and 0.
+                               */
+                               m_clients.send(peer_id, 1, &legacy_pkt, true);
+                               m_clients.send(peer_id, 0, &legacy_pkt, true);
+                       } else {
+                               waiting.emplace(peer_id);
+                               Send(peer_id, &pkt);
+                       }
+               }
        }
+
+       // Run callback for players that already had the file delivered (legacy-only)
+       for (session_t peer_id : delivered) {
+               if (auto player = m_env->getPlayer(peer_id))
+                       getScriptIface()->on_dynamic_media_added(token, player->getName());
+       }
+
+       // Save all others in our pending state
+       auto &state = m_pending_dyn_media[token];
+       state.waiting_players = std::move(waiting);
+       // regardless of success throw away the callback after a while
+       state.expiry_timer = 60.0f;
+       if (ephemeral)
+               state.filename = filename;
+
        return true;
 }
 
@@ -3507,7 +3644,7 @@ bool Server::rollbackRevertActions(const std::list<RollbackAction> &actions,
 
        for (const RollbackAction &action : actions) {
                num_tried++;
-               bool success = action.applyRevert(map, this, this);
+               bool success = action.applyRevert(map, m_inventory_mgr.get(), this);
                if(!success){
                        num_failed++;
                        std::ostringstream os;
@@ -3578,21 +3715,11 @@ const ModSpec *Server::getModSpec(const std::string &modname) const
        return m_modmgr->getModSpec(modname);
 }
 
-void Server::getModNames(std::vector<std::string> &modlist)
-{
-       m_modmgr->getModNames(modlist);
-}
-
 std::string Server::getBuiltinLuaPath()
 {
        return porting::path_share + DIR_DELIM + "builtin";
 }
 
-std::string Server::getModStoragePath() const
-{
-       return m_path_world + DIR_DELIM + "mod_storage";
-}
-
 v3f Server::findSpawnPos()
 {
        ServerMap &map = m_env->getServerMap();
@@ -3609,8 +3736,8 @@ v3f Server::findSpawnPos()
                s32 range = MYMIN(1 + i, range_max);
                // We're going to try to throw the player to this position
                v2s16 nodepos2d = v2s16(
-                       -range + (myrand() % (range * 2)),
-                       -range + (myrand() % (range * 2)));
+                       -range + myrand_range(0, range*2),
+                       -range + myrand_range(0, range*2));
                // Get spawn level at point
                s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d);
                // Continue if MAX_MAP_GENERATION_LIMIT was returned by the mapgen to
@@ -3741,28 +3868,6 @@ PlayerSAO* Server::emergePlayer(const char *name, session_t peer_id, u16 proto_v
        return playersao;
 }
 
-bool Server::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 Server::unregisterModStorage(const std::string &name)
-{
-       std::unordered_map<std::string, ModMetadata *>::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);
-       }
-}
-
 void dedicated_server_loop(Server &server, bool &kill)
 {
        verbosestream<<"dedicated_server_loop()"<<std::endl;
@@ -3873,3 +3978,139 @@ void Server::broadcastModChannelMessage(const std::string &channel,
                m_script->on_modchannel_message(channel, sender, message);
        }
 }
+
+Translations *Server::getTranslationLanguage(const std::string &lang_code)
+{
+       if (lang_code.empty())
+               return nullptr;
+
+       auto it = server_translations.find(lang_code);
+       if (it != server_translations.end())
+               return &it->second; // Already loaded
+
+       // [] will create an entry
+       auto *translations = &server_translations[lang_code];
+
+       std::string suffix = "." + lang_code + ".tr";
+       for (const auto &i : m_media) {
+               if (str_ends_with(i.first, suffix)) {
+                       std::string data;
+                       if (fs::ReadFile(i.second.path, data)) {
+                               translations->loadTranslation(data);
+                       }
+               }
+       }
+
+       return translations;
+}
+
+ModStorageDatabase *Server::openModStorageDatabase(const std::string &world_path)
+{
+       std::string world_mt_path = world_path + DIR_DELIM + "world.mt";
+       Settings world_mt;
+       if (!world_mt.readConfigFile(world_mt_path.c_str()))
+               throw BaseException("Cannot read world.mt!");
+
+       std::string backend = world_mt.exists("mod_storage_backend") ?
+               world_mt.get("mod_storage_backend") : "files";
+       if (backend == "files")
+               warningstream << "/!\\ You are using the old mod storage files backend. "
+                       << "This backend is deprecated and may be removed in a future release /!\\"
+                       << std::endl << "Switching to SQLite3 is advised, "
+                       << "please read http://wiki.minetest.net/Database_backends." << std::endl;
+
+       return openModStorageDatabase(backend, world_path, world_mt);
+}
+
+ModStorageDatabase *Server::openModStorageDatabase(const std::string &backend,
+               const std::string &world_path, const Settings &world_mt)
+{
+       if (backend == "sqlite3")
+               return new ModStorageDatabaseSQLite3(world_path);
+
+#if USE_POSTGRESQL
+       if (backend == "postgresql") {
+               std::string connect_string;
+               world_mt.getNoEx("pgsql_mod_storage_connection", connect_string);
+               return new ModStorageDatabasePostgreSQL(connect_string);
+       }
+#endif // USE_POSTGRESQL
+
+       if (backend == "files")
+               return new ModStorageDatabaseFiles(world_path);
+
+       if (backend == "dummy")
+               return new Database_Dummy();
+
+       throw BaseException("Mod storage database backend " + backend + " not supported");
+}
+
+bool Server::migrateModStorageDatabase(const GameParams &game_params, const Settings &cmd_args)
+{
+       std::string migrate_to = cmd_args.get("migrate-mod-storage");
+       Settings world_mt;
+       std::string world_mt_path = game_params.world_path + DIR_DELIM + "world.mt";
+       if (!world_mt.readConfigFile(world_mt_path.c_str())) {
+               errorstream << "Cannot read world.mt!" << std::endl;
+               return false;
+       }
+
+       std::string backend = world_mt.exists("mod_storage_backend") ?
+               world_mt.get("mod_storage_backend") : "files";
+       if (backend == migrate_to) {
+               errorstream << "Cannot migrate: new backend is same"
+                       << " as the old one" << std::endl;
+               return false;
+       }
+
+       ModStorageDatabase *srcdb = nullptr;
+       ModStorageDatabase *dstdb = nullptr;
+
+       bool succeeded = false;
+
+       try {
+               srcdb = Server::openModStorageDatabase(backend, game_params.world_path, world_mt);
+               dstdb = Server::openModStorageDatabase(migrate_to, game_params.world_path, world_mt);
+
+               dstdb->beginSave();
+
+               std::vector<std::string> mod_list;
+               srcdb->listMods(&mod_list);
+               for (const std::string &modname : mod_list) {
+                       StringMap meta;
+                       srcdb->getModEntries(modname, &meta);
+                       for (const auto &pair : meta) {
+                               dstdb->setModEntry(modname, pair.first, pair.second);
+                       }
+               }
+
+               dstdb->endSave();
+
+               succeeded = true;
+
+               actionstream << "Successfully migrated the metadata of "
+                       << mod_list.size() << " mods" << std::endl;
+               world_mt.set("mod_storage_backend", migrate_to);
+               if (!world_mt.updateConfigFile(world_mt_path.c_str()))
+                       errorstream << "Failed to update world.mt!" << std::endl;
+               else
+                       actionstream << "world.mt updated" << std::endl;
+
+       } catch (BaseException &e) {
+               errorstream << "An error occurred during migration: " << e.what() << std::endl;
+       }
+
+       delete srcdb;
+       delete dstdb;
+
+       if (succeeded && backend == "files") {
+               // Back up files
+               const std::string storage_path = game_params.world_path + DIR_DELIM + "mod_storage";
+               const std::string backup_path = game_params.world_path + DIR_DELIM + "mod_storage.bak";
+               if (!fs::Rename(storage_path, backup_path))
+                       warningstream << "After migration, " << storage_path
+                               << " could not be renamed to " << backup_path << std::endl;
+       }
+
+       return succeeded;
+}