]> git.lizzy.rs Git - minetest.git/blobdiff - src/network/serverpackethandler.cpp
Dual wielding
[minetest.git] / src / network / serverpackethandler.cpp
index bb5eed92b6cb96ba5d5f5c6f98861dd993266171..1e49538dc863a9babddd44952e9d3209b46b3fe3 100644 (file)
@@ -20,7 +20,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "chatmessage.h"
 #include "server.h"
 #include "log.h"
-#include "content_sao.h"
 #include "emerge.h"
 #include "mapblock.h"
 #include "modchannels.h"
@@ -34,11 +33,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "network/connection.h"
 #include "network/networkprotocol.h"
 #include "network/serveropcodes.h"
+#include "server/player_sao.h"
+#include "server/serverinventorymgr.h"
 #include "util/auth.h"
 #include "util/base64.h"
 #include "util/pointedthing.h"
 #include "util/serialize.h"
 #include "util/srp.h"
+#include "clientdynamicinfo.h"
 
 void Server::handleCommand_Deprecated(NetworkPacket* pkt)
 {
@@ -52,41 +54,43 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
        if(pkt->getSize() < 1)
                return;
 
-       RemoteClient* client = getClient(pkt->getPeerId(), CS_Created);
+       session_t peer_id = pkt->getPeerId();
+       RemoteClient *client = getClient(peer_id, CS_Created);
 
+       Address addr;
        std::string addr_s;
        try {
-               Address address = getPeerAddress(pkt->getPeerId());
-               addr_s = address.serializeString();
-       }
-       catch (con::PeerNotFoundException &e) {
+               addr = m_con->GetPeerAddress(peer_id);
+               addr_s = addr.serializeString();
+       } catch (con::PeerNotFoundException &e) {
                /*
                 * no peer for this packet found
                 * most common reason is peer timeout, e.g. peer didn't
                 * respond for some time, your server was overloaded or
                 * things like that.
                 */
-               infostream << "Server::ProcessData(): Canceling: peer "
-                               << pkt->getPeerId() << " not found" << std::endl;
+               infostream << "Server::ProcessData(): Canceling: peer " << peer_id <<
+                       " not found" << std::endl;
                return;
        }
 
-       // If net_proto_version is set, this client has already been handled
        if (client->getState() > CS_Created) {
-               verbosestream << "Server: Ignoring multiple TOSERVER_INITs from "
-                               << addr_s << " (peer_id=" << pkt->getPeerId() << ")" << std::endl;
+               verbosestream << "Server: Ignoring multiple TOSERVER_INITs from " <<
+                       addr_s << " (peer_id=" << peer_id << ")" << std::endl;
                return;
        }
 
-       verbosestream << "Server: Got TOSERVER_INIT from " << addr_s << " (peer_id="
-                       << pkt->getPeerId() << ")" << std::endl;
+       client->setCachedAddress(addr);
+
+       verbosestream << "Server: Got TOSERVER_INIT from " << addr_s <<
+               " (peer_id=" << peer_id << ")" << std::endl;
 
        // Do not allow multiple players in simple singleplayer mode.
        // This isn't a perfect way to do it, but will suffice for now
-       if (m_simple_singleplayer_mode && m_clients.getClientIDs().size() > 1) {
-               infostream << "Server: Not allowing another client (" << addr_s
-                               << ") to connect in simple singleplayer mode" << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_SINGLEPLAYER);
+       if (m_simple_singleplayer_mode && !m_clients.getClientIDs().empty()) {
+               infostream << "Server: Not allowing another client (" << addr_s <<
+                       ") to connect in simple singleplayer mode" << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_SINGLEPLAYER);
                return;
        }
 
@@ -105,15 +109,15 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
        // Use the highest version supported by both
        u8 depl_serial_v = std::min(client_max, our_max);
        // If it's lower than the lowest supported, give up.
+#if SER_FMT_VER_LOWEST_READ > 0
        if (depl_serial_v < SER_FMT_VER_LOWEST_READ)
                depl_serial_v = SER_FMT_VER_INVALID;
+#endif
 
        if (depl_serial_v == SER_FMT_VER_INVALID) {
-               actionstream << "Server: A mismatched client tried to connect from "
-                               << addr_s << std::endl;
-               infostream<<"Server: Cannot negotiate serialization version with "
-                               << addr_s << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_VERSION);
+               actionstream << "Server: A mismatched client tried to connect from " <<
+                       addr_s << " ser_fmt_max=" << (int)client_max << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_VERSION);
                return;
        }
 
@@ -146,9 +150,9 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                        net_proto_version != LATEST_PROTOCOL_VERSION) ||
                        net_proto_version < SERVER_PROTOCOL_VERSION_MIN ||
                        net_proto_version > SERVER_PROTOCOL_VERSION_MAX) {
-               actionstream << "Server: A mismatched client tried to connect from "
-                               << addr_s << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_VERSION);
+               actionstream << "Server: A mismatched client tried to connect from " <<
+                       addr_s << " proto_max=" << (int)max_net_proto_version << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_VERSION);
                return;
        }
 
@@ -159,56 +163,66 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
 
        size_t pns = playerName.size();
        if (pns == 0 || pns > PLAYERNAME_SIZE) {
-               actionstream << "Server: Player with "
-                       << ((pns > PLAYERNAME_SIZE) ? "a too long" : "an empty")
-                       << " name tried to connect from " << addr_s << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_NAME);
+               actionstream << "Server: Player with " <<
+                       ((pns > PLAYERNAME_SIZE) ? "a too long" : "an empty") <<
+                       " name tried to connect from " << addr_s << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_NAME);
                return;
        }
 
        if (!string_allowed(playerName, PLAYERNAME_ALLOWED_CHARS)) {
-               actionstream << "Server: Player with an invalid name "
-                               << "tried to connect from " << addr_s << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_CHARS_IN_NAME);
+               actionstream << "Server: Player with an invalid name tried to connect "
+                       "from " << addr_s << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_CHARS_IN_NAME);
                return;
        }
 
-       m_clients.setPlayerName(pkt->getPeerId(), playername);
+       RemotePlayer *player = m_env->getPlayer(playername);
+
+       // If player is already connected, cancel
+       if (player && player->getPeerId() != PEER_ID_INEXISTENT) {
+               actionstream << "Server: Player with name \"" << playername <<
+                       "\" tried to connect, but player with same name is already connected" << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_ALREADY_CONNECTED);
+               return;
+       }
+
+       m_clients.setPlayerName(peer_id, playername);
        //TODO (later) case insensitivity
 
        std::string legacyPlayerNameCasing = playerName;
 
        if (!isSingleplayer() && strcasecmp(playername, "singleplayer") == 0) {
-               actionstream << "Server: Player with the name \"singleplayer\" "
-                               << "tried to connect from " << addr_s << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_NAME);
+               actionstream << "Server: Player with the name \"singleplayer\" tried "
+                       "to connect from " << addr_s << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_NAME);
                return;
        }
 
        {
                std::string reason;
                if (m_script->on_prejoinplayer(playername, addr_s, &reason)) {
-                       actionstream << "Server: Player with the name \"" << playerName << "\" "
-                                       << "tried to connect from " << addr_s << " "
-                                       << "but it was disallowed for the following reason: "
-                                       << reason << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_CUSTOM_STRING, reason);
+                       actionstream << "Server: Player with the name \"" << playerName <<
+                               "\" tried to connect from " << addr_s <<
+                               " but it was disallowed for the following reason: " << reason <<
+                               std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_CUSTOM_STRING, reason);
                        return;
                }
        }
 
-       infostream << "Server: New connection: \"" << playerName << "\" from "
-                       << addr_s << " (peer_id=" << pkt->getPeerId() << ")" << std::endl;
+       infostream << "Server: New connection: \"" << playerName << "\" from " <<
+               addr_s << " (peer_id=" << peer_id << ")" << std::endl;
 
        // Enforce user limit.
        // Don't enforce for users that have some admin right or mod permits it.
        if (m_clients.isUserLimitReached() &&
                        playername != g_settings->get("name") &&
                        !m_script->can_bypass_userlimit(playername, addr_s)) {
-               actionstream << "Server: " << playername << " tried to join from "
-                               << addr_s << ", but there" << " are already max_users="
-                               << g_settings->getU16("max_users") << " players." << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_TOO_MANY_USERS);
+               actionstream << "Server: " << playername << " tried to join from " <<
+                       addr_s << ", but there are already max_users=" <<
+                       g_settings->getU16("max_users") << " players." << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_TOO_MANY_USERS);
                return;
        }
 
@@ -216,7 +230,7 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                Compose auth methods for answer
        */
        std::string encpwd; // encrypted Password field for the user
-       bool has_auth = m_script->getAuth(playername, &encpwd, NULL);
+       bool has_auth = m_script->getAuth(playername, &encpwd, nullptr);
        u32 auth_mechs = 0;
 
        client->chosen_mech = AUTH_MECHANISM_NONE;
@@ -228,20 +242,19 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                                auth_mechs |= AUTH_MECHANISM_SRP;
                                client->enc_pwd = encpwd;
                        } else {
-                               actionstream << "User " << playername
-                                       << " tried to log in, but password field"
-                                       << " was invalid (unknown mechcode)." << std::endl;
-                               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_SERVER_FAIL);
+                               actionstream << "User " << playername << " tried to log in, "
+                                       "but password field was invalid (unknown mechcode)." <<
+                                       std::endl;
+                               DenyAccess(peer_id, SERVER_ACCESSDENIED_SERVER_FAIL);
                                return;
                        }
                } else if (base64_is_valid(encpwd)) {
                        auth_mechs |= AUTH_MECHANISM_LEGACY_PASSWORD;
                        client->enc_pwd = encpwd;
                } else {
-                       actionstream << "User " << playername
-                               << " tried to log in, but password field"
-                               << " was invalid (invalid base64)." << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_SERVER_FAIL);
+                       actionstream << "User " << playername << " tried to log in, but "
+                               "password field was invalid (invalid base64)." << std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_SERVER_FAIL);
                        return;
                }
        } else {
@@ -264,8 +277,8 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
        verbosestream << "Sending TOCLIENT_HELLO with auth method field: "
                << auth_mechs << std::endl;
 
-       NetworkPacket resp_pkt(TOCLIENT_HELLO, 1 + 4
-               + legacyPlayerNameCasing.size(), pkt->getPeerId());
+       NetworkPacket resp_pkt(TOCLIENT_HELLO,
+               1 + 4 + legacyPlayerNameCasing.size(), peer_id);
 
        u16 depl_compress_mode = NETPROTO_COMPRESSION_NONE;
        resp_pkt << depl_serial_v << depl_compress_mode << net_proto_version
@@ -276,16 +289,16 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
        client->allowed_auth_mechs = auth_mechs;
        client->setDeployedCompressionMode(depl_compress_mode);
 
-       m_clients.event(pkt->getPeerId(), CSE_Hello);
+       m_clients.event(peer_id, CSE_Hello);
 }
 
 void Server::handleCommand_Init2(NetworkPacket* pkt)
 {
-       verbosestream << "Server: Got TOSERVER_INIT2 from "
-                       << pkt->getPeerId() << std::endl;
+       session_t peer_id = pkt->getPeerId();
+       verbosestream << "Server: Got TOSERVER_INIT2 from " << peer_id << std::endl;
 
-       m_clients.event(pkt->getPeerId(), CSE_GotInit2);
-       u16 protocol_version = m_clients.getProtocolVersion(pkt->getPeerId());
+       m_clients.event(peer_id, CSE_GotInit2);
+       u16 protocol_version = m_clients.getProtocolVersion(peer_id);
 
        std::string lang;
        if (pkt->getSize() > 0)
@@ -295,39 +308,50 @@ void Server::handleCommand_Init2(NetworkPacket* pkt)
                Send some initialization data
        */
 
-       infostream << "Server: Sending content to "
-                       << getPlayerName(pkt->getPeerId()) << std::endl;
-
-       // Send player movement settings
-       SendMovement(pkt->getPeerId());
+       infostream << "Server: Sending content to " << getPlayerName(peer_id) <<
+               std::endl;
 
        // Send item definitions
-       SendItemDef(pkt->getPeerId(), m_itemdef, protocol_version);
+       SendItemDef(peer_id, m_itemdef, protocol_version);
 
        // Send node definitions
-       SendNodeDef(pkt->getPeerId(), m_nodedef, protocol_version);
+       SendNodeDef(peer_id, m_nodedef, protocol_version);
 
-       m_clients.event(pkt->getPeerId(), CSE_SetDefinitionsSent);
+       m_clients.event(peer_id, CSE_SetDefinitionsSent);
 
        // Send media announcement
-       sendMediaAnnouncement(pkt->getPeerId(), lang);
+       sendMediaAnnouncement(peer_id, lang);
+
+       RemoteClient *client = getClient(peer_id, CS_InitDone);
+
+       // Keep client language for server translations
+       client->setLangCode(lang);
+
+       // Send active objects
+       {
+               PlayerSAO *sao = getPlayerSAO(peer_id);
+               if (sao)
+                       SendActiveObjectRemoveAdd(client, sao);
+       }
 
        // Send detached inventories
-       sendDetachedInventories(pkt->getPeerId());
+       sendDetachedInventories(peer_id, false);
+
+       // Send player movement settings
+       SendMovement(peer_id);
 
        // Send time of day
        u16 time = m_env->getTimeOfDay();
        float time_speed = g_settings->getFloat("time_speed");
-       SendTimeOfDay(pkt->getPeerId(), time, time_speed);
+       SendTimeOfDay(peer_id, time, time_speed);
 
-       SendCSMFlavourLimits(pkt->getPeerId());
+       SendCSMRestrictionFlags(peer_id);
 
        // Warnings about protocol version can be issued here
-       if (getClient(pkt->getPeerId())->net_proto_version < LATEST_PROTOCOL_VERSION) {
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                               L"# Server: WARNING: YOUR CLIENT'S VERSION MAY NOT BE FULLY COMPATIBLE "
-                               L"WITH THIS SERVER!"));
-
+       if (client->net_proto_version < LATEST_PROTOCOL_VERSION) {
+               SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                       L"# Server: WARNING: YOUR CLIENT'S VERSION MAY NOT BE FULLY COMPATIBLE "
+                       L"WITH THIS SERVER!"));
        }
 }
 
@@ -338,73 +362,68 @@ void Server::handleCommand_RequestMedia(NetworkPacket* pkt)
 
        *pkt >> numfiles;
 
-       infostream << "Sending " << numfiles << " files to "
-                       << getPlayerName(pkt->getPeerId()) << std::endl;
-       verbosestream << "TOSERVER_REQUEST_MEDIA: " << std::endl;
+       session_t peer_id = pkt->getPeerId();
+       infostream << "Sending " << numfiles << " files to " <<
+               getPlayerName(peer_id) << std::endl;
+       verbosestream << "TOSERVER_REQUEST_MEDIA: requested file(s)" << std::endl;
 
        for (u16 i = 0; i < numfiles; i++) {
                std::string name;
 
                *pkt >> name;
 
-               tosend.push_back(name);
-               verbosestream << "TOSERVER_REQUEST_MEDIA: requested file "
-                               << name << std::endl;
+               tosend.emplace_back(name);
+               verbosestream << "  " << name << std::endl;
        }
 
-       sendRequestedMedia(pkt->getPeerId(), tosend);
+       sendRequestedMedia(peer_id, tosend);
 }
 
 void Server::handleCommand_ClientReady(NetworkPacket* pkt)
 {
        session_t peer_id = pkt->getPeerId();
 
-       PlayerSAO* playersao = StageTwoClientInit(peer_id);
+       // decode all information first
+       u8 major_ver, minor_ver, patch_ver, reserved;
+       u16 formspec_ver = 1; // v1 for clients older than 5.1.0-dev
+       std::string full_ver;
 
-       if (playersao == NULL) {
-               actionstream
-                       << "TOSERVER_CLIENT_READY stage 2 client init failed for peer_id: "
-                       << peer_id << std::endl;
-               DisconnectPeer(peer_id);
-               return;
-       }
+       *pkt >> major_ver >> minor_ver >> patch_ver >> reserved >> full_ver;
+       if (pkt->getRemainingBytes() >= 2)
+               *pkt >> formspec_ver;
 
+       m_clients.setClientVersion(peer_id, major_ver, minor_ver, patch_ver,
+               full_ver);
 
-       if (pkt->getSize() < 8) {
-               errorstream
-                       << "TOSERVER_CLIENT_READY client sent inconsistent data, disconnecting peer_id: "
-                       << peer_id << std::endl;
+       // Emerge player
+       PlayerSAO* playersao = StageTwoClientInit(peer_id);
+       if (!playersao) {
+               errorstream << "Server: stage 2 client init failed "
+                       "peer_id=" << peer_id << std::endl;
                DisconnectPeer(peer_id);
                return;
        }
 
-       u8 major_ver, minor_ver, patch_ver, reserved;
-       std::string full_ver;
-       *pkt >> major_ver >> minor_ver >> patch_ver >> reserved >> full_ver;
-
-       m_clients.setClientVersion(
-                       peer_id, major_ver, minor_ver, patch_ver,
-                       full_ver);
+       playersao->getPlayer()->formspec_version = formspec_ver;
+       m_clients.event(peer_id, CSE_SetClientReady);
 
-       const std::vector<std::string> &players = m_clients.getPlayerNames();
-       NetworkPacket list_pkt(TOCLIENT_UPDATE_PLAYER_LIST, 0, peer_id);
-       list_pkt << (u8) PLAYER_LIST_INIT << (u16) players.size();
-       for (const std::string &player: players) {
-               list_pkt <<  player;
+       // Send player list to this client
+       {
+               const std::vector<std::string> &players = m_clients.getPlayerNames();
+               NetworkPacket list_pkt(TOCLIENT_UPDATE_PLAYER_LIST, 0, peer_id);
+               list_pkt << (u8) PLAYER_LIST_INIT << (u16) players.size();
+               for (const auto &player : players)
+                       list_pkt << player;
+               Send(peer_id, &list_pkt);
        }
-       m_clients.send(peer_id, 0, &list_pkt, true);
 
-       NetworkPacket notice_pkt(TOCLIENT_UPDATE_PLAYER_LIST, 0, PEER_ID_INEXISTENT);
-       // (u16) 1 + std::string represents a pseudo vector serialization representation
-       notice_pkt << (u8) PLAYER_LIST_ADD << (u16) 1 << std::string(playersao->getPlayer()->getName());
-       m_clients.sendToAll(&notice_pkt);
+       s64 last_login;
+       m_script->getAuth(playersao->getPlayer()->getName(), nullptr, nullptr, &last_login);
+       m_script->on_joinplayer(playersao, last_login);
 
-       m_clients.event(peer_id, CSE_SetClientReady);
-       m_script->on_joinplayer(playersao);
        // Send shutdown timer if shutdown has been scheduled
-       if (m_shutdown_state.isTimerRunning()) {
-               SendChatMessage(pkt->getPeerId(), m_shutdown_state.getShutdownTimerMessage());
-       }
+       if (m_shutdown_state.isTimerRunning())
+               SendChatMessage(peer_id, m_shutdown_state.getShutdownTimerMessage());
 }
 
 void Server::handleCommand_GotBlocks(NetworkPacket* pkt)
@@ -423,13 +442,14 @@ void Server::handleCommand_GotBlocks(NetworkPacket* pkt)
        u8 count;
        *pkt >> count;
 
-       RemoteClient *client = getClient(pkt->getPeerId());
-
        if ((s16)pkt->getSize() < 1 + (int)count * 6) {
                throw con::InvalidIncomingDataException
                                ("GOTBLOCKS length is too short");
        }
 
+       ClientInterface::AutoLock lock(m_clients);
+       RemoteClient *client = m_clients.lockedGetClientNoEx(pkt->getPeerId());
+
        for (u16 i = 0; i < count; i++) {
                v3s16 p;
                *pkt >> p;
@@ -456,7 +476,6 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao,
        f32 yaw = (f32)f32yaw / 100.0f;
        u32 keyPressed = 0;
 
-       // default behavior (in case an old client doesn't send these)
        f32 fov = 0;
        u8 wanted_range = 0;
 
@@ -471,22 +490,18 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao,
        pitch = modulo360f(pitch);
        yaw = wrapDegrees_0_360(yaw);
 
-       playersao->setBasePosition(position);
-       player->setSpeed(speed);
-       playersao->setPitch(pitch);
-       playersao->setYaw(yaw);
+       if (!playersao->isAttached()) {
+               // Only update player positions when moving freely
+               // to not interfere with attachment handling
+               playersao->setBasePosition(position);
+               player->setSpeed(speed);
+       }
+       playersao->setLookPitch(pitch);
+       playersao->setPlayerYaw(yaw);
        playersao->setFov(fov);
        playersao->setWantedRange(wanted_range);
-       player->keyPressed = keyPressed;
-       player->control.up = (keyPressed & 1);
-       player->control.down = (keyPressed & 2);
-       player->control.left = (keyPressed & 4);
-       player->control.right = (keyPressed & 8);
-       player->control.jump = (keyPressed & 16);
-       player->control.aux1 = (keyPressed & 32);
-       player->control.sneak = (keyPressed & 64);
-       player->control.LMB = (keyPressed & 128);
-       player->control.RMB = (keyPressed & 256);
+
+       player->control.unpackKeysPressed(keyPressed);
 
        if (playersao->checkMovementCheat()) {
                // Call callbacks
@@ -497,21 +512,22 @@ void Server::process_PlayerPos(RemotePlayer *player, PlayerSAO *playersao,
 
 void Server::handleCommand_PlayerPos(NetworkPacket* pkt)
 {
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -557,22 +573,23 @@ void Server::handleCommand_DeletedBlocks(NetworkPacket* pkt)
 
 void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
 {
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -582,7 +599,7 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                << std::endl;
        std::istringstream is(datastring, std::ios_base::binary);
        // Create an action
-       InventoryAction *a = InventoryAction::deSerialize(is);
+       std::unique_ptr<InventoryAction> a(InventoryAction::deSerialize(is));
        if (!a) {
                infostream << "TOSERVER_INVENTORY_ACTION: "
                                << "InventoryAction::deSerialize() returned NULL"
@@ -599,37 +616,56 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                where the client made a bad prediction.
        */
 
+       const bool player_has_interact = checkPriv(player->getName(), "interact");
+
+       auto check_inv_access = [player, player_has_interact, this] (
+                       const InventoryLocation &loc) -> bool {
+
+               // Players without interact may modify their own inventory
+               if (!player_has_interact && loc.type != InventoryLocation::PLAYER) {
+                       infostream << "Cannot modify foreign inventory: "
+                                       << "No interact privilege" << std::endl;
+                       return false;
+               }
+
+               switch (loc.type) {
+               case InventoryLocation::CURRENT_PLAYER:
+                       // Only used internally on the client, never sent
+                       return false;
+               case InventoryLocation::PLAYER:
+                       // Allow access to own inventory in all cases
+                       return loc.name == player->getName();
+               case InventoryLocation::NODEMETA:
+                       {
+                               // Check for out-of-range interaction
+                               v3f node_pos   = intToFloat(loc.p, BS);
+                               v3f player_pos = player->getPlayerSAO()->getEyePosition();
+                               f32 d = player_pos.getDistanceFrom(node_pos);
+                               return checkInteractDistance(player, d, "inventory");
+                       }
+               case InventoryLocation::DETACHED:
+                       return getInventoryMgr()->checkDetachedInventoryAccess(loc, player->getName());
+               default:
+                       return false;
+               }
+       };
+
        /*
                Handle restrictions and special cases of the move action
        */
        if (a->getType() == IAction::Move) {
-               IMoveAction *ma = (IMoveAction*)a;
+               IMoveAction *ma = (IMoveAction*)a.get();
 
                ma->from_inv.applyCurrentPlayer(player->getName());
                ma->to_inv.applyCurrentPlayer(player->getName());
 
-               setInventoryModified(ma->from_inv, false);
-               setInventoryModified(ma->to_inv, false);
-
-               bool from_inv_is_current_player =
-                       (ma->from_inv.type == InventoryLocation::PLAYER) &&
-                       (ma->from_inv.name == player->getName());
-
-               bool to_inv_is_current_player =
-                       (ma->to_inv.type == InventoryLocation::PLAYER) &&
-                       (ma->to_inv.name == player->getName());
+               m_inventory_mgr->setInventoryModified(ma->from_inv);
+               if (ma->from_inv != ma->to_inv)
+                       m_inventory_mgr->setInventoryModified(ma->to_inv);
 
-               InventoryLocation *remote = from_inv_is_current_player ?
-                       &ma->to_inv : &ma->from_inv;
-
-               // Check for out-of-range interaction
-               if (remote->type == InventoryLocation::NODEMETA) {
-                       v3f node_pos   = intToFloat(remote->p, BS);
-                       v3f player_pos = player->getPlayerSAO()->getBasePosition();
-                       f32 d = player_pos.getDistanceFrom(node_pos);
-                       if (!checkInteractDistance(player, d, "inventory"))
-                               return;
-               }
+               if (!check_inv_access(ma->from_inv) ||
+                               !check_inv_access(ma->to_inv))
+                       return;
 
                /*
                        Disable moving items out of craftpreview
@@ -639,7 +675,6 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                                        << (ma->from_inv.dump()) << ":" << ma->from_list
                                        << " to " << (ma->to_inv.dump()) << ":" << ma->to_list
                                        << " because src is " << ma->from_list << std::endl;
-                       delete a;
                        return;
                }
 
@@ -651,18 +686,6 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                                        << (ma->from_inv.dump()) << ":" << ma->from_list
                                        << " to " << (ma->to_inv.dump()) << ":" << ma->to_list
                                        << " because dst is " << ma->to_list << std::endl;
-                       delete a;
-                       return;
-               }
-
-               // Disallow moving items in elsewhere than player's inventory
-               // if not allowed to interact
-               if (!checkPriv(player->getName(), "interact") &&
-                               (!from_inv_is_current_player ||
-                               !to_inv_is_current_player)) {
-                       infostream << "Cannot move outside of player's inventory: "
-                                       << "No interact privilege" << std::endl;
-                       delete a;
                        return;
                }
        }
@@ -670,11 +693,11 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                Handle restrictions and special cases of the drop action
        */
        else if (a->getType() == IAction::Drop) {
-               IDropAction *da = (IDropAction*)a;
+               IDropAction *da = (IDropAction*)a.get();
 
                da->from_inv.applyCurrentPlayer(player->getName());
 
-               setInventoryModified(da->from_inv, false);
+               m_inventory_mgr->setInventoryModified(da->from_inv);
 
                /*
                        Disable dropping items out of craftpreview
@@ -683,22 +706,18 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                        infostream << "Ignoring IDropAction from "
                                        << (da->from_inv.dump()) << ":" << da->from_list
                                        << " because src is " << da->from_list << std::endl;
-                       delete a;
                        return;
                }
 
                // Disallow dropping items if not allowed to interact
-               if (!checkPriv(player->getName(), "interact")) {
-                       delete a;
+               if (!player_has_interact || !check_inv_access(da->from_inv))
                        return;
-               }
 
                // Disallow dropping items if dead
                if (playersao->isDead()) {
                        infostream << "Ignoring IDropAction from "
                                        << (da->from_inv.dump()) << ":" << da->from_list
                                        << " because player is dead." << std::endl;
-                       delete a;
                        return;
                }
        }
@@ -706,98 +725,82 @@ void Server::handleCommand_InventoryAction(NetworkPacket* pkt)
                Handle restrictions and special cases of the craft action
        */
        else if (a->getType() == IAction::Craft) {
-               ICraftAction *ca = (ICraftAction*)a;
+               ICraftAction *ca = (ICraftAction*)a.get();
 
                ca->craft_inv.applyCurrentPlayer(player->getName());
 
-               setInventoryModified(ca->craft_inv, false);
-
-               //bool craft_inv_is_current_player =
-               //      (ca->craft_inv.type == InventoryLocation::PLAYER) &&
-               //      (ca->craft_inv.name == player->getName());
+               m_inventory_mgr->setInventoryModified(ca->craft_inv);
 
                // Disallow crafting if not allowed to interact
-               if (!checkPriv(player->getName(), "interact")) {
+               if (!player_has_interact) {
                        infostream << "Cannot craft: "
                                        << "No interact privilege" << std::endl;
-                       delete a;
                        return;
                }
+
+               if (!check_inv_access(ca->craft_inv))
+                       return;
+       } else {
+               // Unknown action. Ignored.
+               return;
        }
 
        // Do the action
-       a->apply(this, playersao, this);
-       // Eat the action
-       delete a;
-
-       SendInventory(playersao);
+       a->apply(m_inventory_mgr.get(), playersao, this);
 }
 
 void Server::handleCommand_ChatMessage(NetworkPacket* pkt)
 {
-       /*
-               u16 command
-               u16 length
-               wstring message
-       */
-       u16 len;
-       *pkt >> len;
-
        std::wstring message;
-       for (u16 i = 0; i < len; i++) {
-               u16 tmp_wchar;
-               *pkt >> tmp_wchar;
-
-               message += (wchar_t)tmp_wchar;
-       }
+       *pkt >> message;
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
-       // Get player name of this client
        std::string name = player->getName();
-       std::wstring wname = narrow_to_wide(name);
 
-       std::wstring answer_to_sender = handleChat(name, wname, message, true, player);
+       std::wstring answer_to_sender = handleChat(name, message, true, player);
        if (!answer_to_sender.empty()) {
                // Send the answer to sender
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_NORMAL,
-                               answer_to_sender, wname));
+               SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                       answer_to_sender));
        }
 }
 
 void Server::handleCommand_Damage(NetworkPacket* pkt)
 {
-       u8 damage;
+       u16 damage;
 
        *pkt >> damage;
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
-       if (g_settings->getBool("enable_damage")) {
+       if (!playersao->isImmortal()) {
                if (playersao->isDead()) {
                        verbosestream << "Server::ProcessData(): Info: "
                                "Ignoring damage as player " << player->getName()
@@ -810,86 +813,7 @@ void Server::handleCommand_Damage(NetworkPacket* pkt)
                                << std::endl;
 
                PlayerHPChangeReason reason(PlayerHPChangeReason::FALL);
-               playersao->setHP(playersao->getHP() - damage, reason);
-               SendPlayerHPOrDie(playersao, reason);
-       }
-}
-
-void Server::handleCommand_Password(NetworkPacket* pkt)
-{
-       if (pkt->getSize() != PASSWORD_SIZE * 2)
-               return;
-
-       std::string oldpwd;
-       std::string newpwd;
-
-       // Deny for clients using the new protocol
-       RemoteClient* client = getClient(pkt->getPeerId(), CS_Created);
-       if (client->net_proto_version >= 25) {
-               infostream << "Server::handleCommand_Password(): Denying change: "
-                       << " Client protocol version for peer_id=" << pkt->getPeerId()
-                       << " too new!" << std::endl;
-               return;
-       }
-
-       for (u16 i = 0; i < PASSWORD_SIZE - 1; i++) {
-               char c = pkt->getChar(i);
-               if (c == 0)
-                       break;
-               oldpwd += c;
-       }
-
-       for (u16 i = 0; i < PASSWORD_SIZE - 1; i++) {
-               char c = pkt->getChar(PASSWORD_SIZE + i);
-               if (c == 0)
-                       break;
-               newpwd += c;
-       }
-
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
-       if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
-               return;
-       }
-
-       if (!base64_is_valid(newpwd)) {
-               infostream<<"Server: " << player->getName() <<
-                               " supplied invalid password hash" << std::endl;
-               // Wrong old password supplied!!
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                               L"Invalid new password hash supplied. Password NOT changed."));
-               return;
-       }
-
-       infostream << "Server: Client requests a password change from "
-                       << "'" << oldpwd << "' to '" << newpwd << "'" << std::endl;
-
-       std::string playername = player->getName();
-
-       std::string checkpwd;
-       m_script->getAuth(playername, &checkpwd, NULL);
-
-       if (oldpwd != checkpwd) {
-               infostream << "Server: invalid old password" << std::endl;
-               // Wrong old password supplied!!
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                               L"Invalid old password supplied. Password NOT changed."));
-               return;
-       }
-
-       bool success = m_script->setPassword(playername, newpwd);
-       if (success) {
-               actionstream << player->getName() << " changes password" << std::endl;
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                               L"Password change successful."));
-       } else {
-               actionstream << player->getName() << " tries to change password but "
-                               << "it fails" << std::endl;
-               SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                               L"Password change failed or unavailable."));
+               playersao->setHP((s32)playersao->getHP() - (s32)damage, reason, true);
        }
 }
 
@@ -898,22 +822,23 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt)
        if (pkt->getSize() < 2)
                return;
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -921,17 +846,27 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt)
 
        *pkt >> item;
 
-       playersao->setWieldIndex(item);
+       if (item >= player->getHotbarItemcount()) {
+               actionstream << "Player: " << player->getName()
+                       << " tried to access item=" << item
+                       << " out of hotbar_itemcount="
+                       << player->getHotbarItemcount()
+                       << "; ignoring." << std::endl;
+               return;
+       }
+
+       playersao->getPlayer()->setWieldIndex(item);
 }
 
 void Server::handleCommand_Respawn(NetworkPacket* pkt)
 {
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -941,7 +876,7 @@ void Server::handleCommand_Respawn(NetworkPacket* pkt)
        if (!playersao->isDead())
                return;
 
-       RespawnPlayer(pkt->getPeerId());
+       RespawnPlayer(peer_id);
 
        actionstream << player->getName() << " respawns at "
                        << PP(playersao->getBasePosition() / BS) << std::endl;
@@ -950,38 +885,44 @@ void Server::handleCommand_Respawn(NetworkPacket* pkt)
        // the previous addition has been successfully removed
 }
 
-bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std::string what)
+bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std::string &what)
 {
-       PlayerSAO *playersao = player->getPlayerSAO();
-       const InventoryList *hlist = playersao->getInventory()->getList("hand");
-       const ItemDefinition &playeritem_def =
-               playersao->getWieldedItem().getDefinition(m_itemdef);
-       const ItemDefinition &hand_def =
-               hlist ? hlist->getItem(0).getDefinition(m_itemdef) : m_itemdef->get("");
-
-       float max_d = BS * playeritem_def.range;
-       float max_d_hand = BS * hand_def.range;
-
-       if (max_d < 0 && max_d_hand >= 0)
-               max_d = max_d_hand;
-       else if (max_d < 0)
-               max_d = BS * 4.0f;
-
-       // cube diagonal: sqrt(3) = 1.732
-       if (d > max_d * 1.732) {
+       ItemStack selected_item, main_item;
+       player->getWieldedItem(&selected_item, &main_item);
+       f32 max_d = BS * getToolRange(selected_item.getDefinition(m_itemdef),
+                       main_item.getDefinition(m_itemdef));
+
+       // Cube diagonal * 1.5 for maximal supported node extents:
+       // sqrt(3) * 1.5 â‰… 2.6
+       if (d > max_d + 2.6f * BS) {
                actionstream << "Player " << player->getName()
                                << " tried to access " << what
                                << " from too far: "
-                               << "d=" << d <<", max_d=" << max_d
-                               << ". ignoring." << std::endl;
+                               << "d=" << d << ", max_d=" << max_d
+                               << "; ignoring." << std::endl;
                // Call callbacks
-               m_script->on_cheat(playersao, "interacted_too_far");
+               m_script->on_cheat(player->getPlayerSAO(), "interacted_too_far");
                return false;
        }
        return true;
 }
 
-void Server::handleCommand_Interact(NetworkPacket* pkt)
+// Tiny helper to retrieve the selected item into an Optional
+static inline void getWieldedItem(const PlayerSAO *playersao, Optional<ItemStack> &ret)
+{
+       ret = ItemStack();
+       playersao->getWieldedItem(&(*ret));
+}
+
+static inline bool getOffhandWieldedItem(const PlayerSAO *playersao, Optional<ItemStack> &offhand,
+       Optional<ItemStack> &place, IItemDefManager *idef, const PointedThing &pointed)
+{
+       offhand = ItemStack();
+       place = ItemStack();
+       return playersao->getOffhandWieldedItem(&(*offhand), &(*place), idef, pointed);
+}
+
+void Server::handleCommand_Interact(NetworkPacket *pkt)
 {
        /*
                [0] u16 command
@@ -990,18 +931,14 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
                [5] u32 length of the next item (plen)
                [9] serialized PointedThing
                [9 + plen] player position information
-               actions:
-               0: start digging (from undersurface) or use
-               1: stop digging (all parameters ignored)
-               2: digging completed
-               3: place block or item (to abovesurface)
-               4: use item
-               5: rightclick air ("activate")
        */
-       u8 action;
+
+       InteractAction action;
        u16 item_i;
-       *pkt >> action;
+
+       *pkt >> (u8 &)action;
        *pkt >> item_i;
+
        std::istringstream tmp_is(pkt->readLongString(), std::ios::binary);
        PointedThing pointed;
        pointed.deSerialize(tmp_is);
@@ -1009,31 +946,32 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
        verbosestream << "TOSERVER_INTERACT: action=" << (int)action << ", item="
                        << item_i << ", pointed=" << pointed.dump() << std::endl;
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        if (playersao->isDead()) {
-               actionstream << "Server: NoCheat: " << player->getName()
+               actionstream << "Server: " << player->getName()
                                << " tried to interact while dead; ignoring." << std::endl;
                if (pointed.type == POINTEDTHING_NODE) {
                        // Re-send block to revert change on client-side
-                       RemoteClient *client = getClient(pkt->getPeerId());
+                       RemoteClient *client = getClient(peer_id);
                        v3s16 blockpos = getNodeBlockPos(pointed.node_undersurface);
                        client->SetBlockNotSent(blockpos);
                }
@@ -1047,11 +985,17 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
        v3f player_pos = playersao->getLastGoodPosition();
 
        // Update wielded item
-       playersao->setWieldIndex(item_i);
 
-       // Get pointed to node (undefined if not POINTEDTYPE_NODE)
-       v3s16 p_under = pointed.node_undersurface;
-       v3s16 p_above = pointed.node_abovesurface;
+       if (item_i >= player->getHotbarItemcount()) {
+               actionstream << "Player: " << player->getName()
+                       << " tried to access item=" << item_i
+                       << " out of hotbar_itemcount="
+                       << player->getHotbarItemcount()
+                       << "; ignoring." << std::endl;
+               return;
+       }
+
+       playersao->getPlayer()->setWieldIndex(item_i);
 
        // Get pointed to object (NULL if not POINTEDTYPE_OBJECT)
        ServerActiveObject *pointed_object = NULL;
@@ -1065,34 +1009,26 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
 
        }
 
-       v3f pointed_pos_under = player_pos;
-       v3f pointed_pos_above = player_pos;
-       if (pointed.type == POINTEDTHING_NODE) {
-               pointed_pos_under = intToFloat(p_under, BS);
-               pointed_pos_above = intToFloat(p_above, BS);
-       }
-       else if (pointed.type == POINTEDTHING_OBJECT) {
-               pointed_pos_under = pointed_object->getBasePosition();
-               pointed_pos_above = pointed_pos_under;
-       }
-
        /*
                Make sure the player is allowed to do it
        */
        if (!checkPriv(player->getName(), "interact")) {
-               actionstream<<player->getName()<<" attempted to interact with "
-                               <<pointed.dump()<<" without 'interact' privilege"
-                               <<std::endl;
+               actionstream << player->getName() << " attempted to interact with " <<
+                               pointed.dump() << " without 'interact' privilege" << std::endl;
+
+               if (pointed.type != POINTEDTHING_NODE)
+                       return;
+
                // Re-send block to revert change on client-side
-               RemoteClient *client = getClient(pkt->getPeerId());
+               RemoteClient *client = getClient(peer_id);
                // Digging completed -> under
-               if (action == 2) {
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+               if (action == INTERACT_DIGGING_COMPLETED) {
+                       v3s16 blockpos = getNodeBlockPos(pointed.node_undersurface);
                        client->SetBlockNotSent(blockpos);
                }
                // Placement -> above
-               else if (action == 3) {
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
+               else if (action == INTERACT_PLACE) {
+                       v3s16 blockpos = getNodeBlockPos(pointed.node_abovesurface);
                        client->SetBlockNotSent(blockpos);
                }
                return;
@@ -1100,19 +1036,34 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
 
        /*
                Check that target is reasonably close
-               (only when digging or placing things)
        */
        static thread_local const bool enable_anticheat =
                        !g_settings->getBool("disable_anticheat");
 
-       if ((action == 0 || action == 2 || action == 3 || action == 4) &&
+       if ((action == INTERACT_START_DIGGING || action == INTERACT_DIGGING_COMPLETED ||
+                       action == INTERACT_PLACE || action == INTERACT_USE) &&
                        enable_anticheat && !isSingleplayer()) {
-               float d = player_pos.getDistanceFrom(pointed_pos_under);
+               v3f target_pos = player_pos;
+               if (pointed.type == POINTEDTHING_NODE) {
+                       target_pos = intToFloat(pointed.node_undersurface, BS);
+               } else if (pointed.type == POINTEDTHING_OBJECT) {
+                       if (playersao->getId() == pointed_object->getId()) {
+                               actionstream << "Server: " << player->getName()
+                                       << " attempted to interact with themselves" << std::endl;
+                               m_script->on_cheat(playersao, "interacted_with_self");
+                               return;
+                       }
+                       target_pos = pointed_object->getBasePosition();
+               }
+               float d = playersao->getEyePosition().getDistanceFrom(target_pos);
+
                if (!checkInteractDistance(player, d, pointed.dump())) {
-                       // Re-send block to revert change on client-side
-                       RemoteClient *client = getClient(pkt->getPeerId());
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
-                       client->SetBlockNotSent(blockpos);
+                       if (pointed.type == POINTEDTHING_NODE) {
+                               // Re-send block to revert change on client-side
+                               RemoteClient *client = getClient(peer_id);
+                               v3s16 blockpos = getNodeBlockPos(pointed.node_undersurface);
+                               client->SetBlockNotSent(blockpos);
+                       }
                        return;
                }
        }
@@ -1123,21 +1074,20 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
        RollbackScopeActor rollback_scope(m_rollback,
                        std::string("player:")+player->getName());
 
-       /*
-               0: start digging or punch object
-       */
-       if (action == 0) {
+       switch (action) {
+       // Start digging or punch object
+       case INTERACT_START_DIGGING: {
                if (pointed.type == POINTEDTHING_NODE) {
                        MapNode n(CONTENT_IGNORE);
                        bool pos_ok;
 
-                       n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
+                       v3s16 p_under = pointed.node_undersurface;
+                       n = m_env->getMap().getNode(p_under, &pos_ok);
                        if (!pos_ok) {
-                               infostream << "Server: Not punching: Node not found."
-                                               << " Adding block to emerge queue."
-                                               << std::endl;
-                               m_emerge->enqueueBlockEmerge(pkt->getPeerId(),
-                                       getNodeBlockPos(p_above), false);
+                               infostream << "Server: Not punching: Node not found. "
+                                       "Adding block to emerge queue." << std::endl;
+                               m_emerge->enqueueBlockEmerge(peer_id,
+                                       getNodeBlockPos(pointed.node_abovesurface), false);
                        }
 
                        if (n.getContent() != CONTENT_IGNORE)
@@ -1145,166 +1095,152 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
 
                        // Cheat prevention
                        playersao->noCheatDigStart(p_under);
+
+                       return;
                }
-               else if (pointed.type == POINTEDTHING_OBJECT) {
-                       // Skip if object can't be interacted with anymore
-                       if (pointed_object->isGone())
-                               return;
 
-                       actionstream<<player->getName()<<" punches object "
-                                       <<pointed.object_id<<": "
-                                       <<pointed_object->getDescription()<<std::endl;
-
-                       ItemStack punchitem = playersao->getWieldedItemOrHand();
-                       ToolCapabilities toolcap =
-                                       punchitem.getToolCapabilities(m_itemdef);
-                       v3f dir = (pointed_object->getBasePosition() -
-                                       (playersao->getBasePosition() + playersao->getEyeOffset())
-                                               ).normalize();
-                       float time_from_last_punch =
-                               playersao->resetTimeFromLastPunch();
-
-                       s16 src_original_hp = pointed_object->getHP();
-                       s16 dst_origin_hp = playersao->getHP();
-
-                       pointed_object->punch(dir, &toolcap, playersao,
-                                       time_from_last_punch);
-
-                       // If the object is a player and its HP changed
-                       if (src_original_hp != pointed_object->getHP() &&
-                                       pointed_object->getType() == ACTIVEOBJECT_TYPE_PLAYER) {
-                               SendPlayerHPOrDie((PlayerSAO *)pointed_object,
-                                               PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, playersao));
-                       }
+               // Skip if the object can't be interacted with anymore
+               if (pointed.type != POINTEDTHING_OBJECT || pointed_object->isGone())
+                       return;
 
-                       // If the puncher is a player and its HP changed
-                       if (dst_origin_hp != playersao->getHP())
-                               SendPlayerHPOrDie(playersao,
-                                               PlayerHPChangeReason(PlayerHPChangeReason::PLAYER_PUNCH, pointed_object));
-               }
+               ItemStack selected_item, hand_item;
+               ItemStack tool_item = playersao->getWieldedItem(&selected_item, &hand_item);
+               ToolCapabilities toolcap =
+                               tool_item.getToolCapabilities(m_itemdef);
+               v3f dir = (pointed_object->getBasePosition() -
+                               (playersao->getBasePosition() + playersao->getEyeOffset())
+                                       ).normalize();
+               float time_from_last_punch =
+                       playersao->resetTimeFromLastPunch();
+
+               u32 wear = pointed_object->punch(dir, &toolcap, playersao,
+                               time_from_last_punch, tool_item.wear);
+
+               // Callback may have changed item, so get it again
+               playersao->getWieldedItem(&selected_item);
+               bool changed = selected_item.addWear(wear, m_itemdef);
+               if (changed)
+                       playersao->setWieldedItem(selected_item);
 
-       } // action == 0
+               return;
+       } // action == INTERACT_START_DIGGING
 
-       /*
-               1: stop digging
-       */
-       else if (action == 1) {
-       } // action == 1
+       case INTERACT_STOP_DIGGING:
+               // Nothing to do
+               return;
 
-       /*
-               2: Digging completed
-       */
-       else if (action == 2) {
+       case INTERACT_DIGGING_COMPLETED: {
                // Only digging of nodes
-               if (pointed.type == POINTEDTHING_NODE) {
-                       bool pos_ok;
-                       MapNode n = m_env->getMap().getNodeNoEx(p_under, &pos_ok);
-                       if (!pos_ok) {
-                               infostream << "Server: Not finishing digging: Node not found."
-                                               << " Adding block to emerge queue."
-                                               << std::endl;
-                               m_emerge->enqueueBlockEmerge(pkt->getPeerId(),
-                                       getNodeBlockPos(p_above), false);
-                       }
+               if (pointed.type != POINTEDTHING_NODE)
+                       return;
+               bool pos_ok;
+               v3s16 p_under = pointed.node_undersurface;
+               MapNode n = m_env->getMap().getNode(p_under, &pos_ok);
+               if (!pos_ok) {
+                       infostream << "Server: Not finishing digging: Node not found. "
+                               "Adding block to emerge queue." << std::endl;
+                       m_emerge->enqueueBlockEmerge(peer_id,
+                               getNodeBlockPos(pointed.node_abovesurface), false);
+               }
 
-                       /* Cheat prevention */
-                       bool is_valid_dig = true;
-                       if (enable_anticheat && !isSingleplayer()) {
-                               v3s16 nocheat_p = playersao->getNoCheatDigPos();
-                               float nocheat_t = playersao->getNoCheatDigTime();
-                               playersao->noCheatDigEnd();
-                               // If player didn't start digging this, ignore dig
-                               if (nocheat_p != p_under) {
-                                       infostream << "Server: NoCheat: " << player->getName()
-                                                       << " started digging "
-                                                       << PP(nocheat_p) << " and completed digging "
-                                                       << PP(p_under) << "; not digging." << std::endl;
-                                       is_valid_dig = false;
-                                       // Call callbacks
-                                       m_script->on_cheat(playersao, "finished_unknown_dig");
-                               }
-                               // Get player's wielded item
-                               ItemStack playeritem = playersao->getWieldedItemOrHand();
-                               ToolCapabilities playeritem_toolcap =
-                                               playeritem.getToolCapabilities(m_itemdef);
-                               // Get diggability and expected digging time
-                               DigParams params = getDigParams(m_nodedef->get(n).groups,
-                                               &playeritem_toolcap);
-                               // If can't dig, try hand
-                               if (!params.diggable) {
-                                       InventoryList *hlist = playersao->getInventory()->getList("hand");
-                                       const ToolCapabilities *tp = hlist
-                                               ? &hlist->getItem(0).getToolCapabilities(m_itemdef)
-                                               : m_itemdef->get("").tool_capabilities;
-
-                                       if (tp)
-                                               params = getDigParams(m_nodedef->get(n).groups, tp);
-                               }
-                               // If can't dig, ignore dig
-                               if (!params.diggable) {
-                                       infostream << "Server: NoCheat: " << player->getName()
-                                                       << " completed digging " << PP(p_under)
-                                                       << ", which is not diggable with tool. not digging."
-                                                       << std::endl;
-                                       is_valid_dig = false;
-                                       // Call callbacks
-                                       m_script->on_cheat(playersao, "dug_unbreakable");
-                               }
-                               // Check digging time
-                               // If already invalidated, we don't have to
-                               if (!is_valid_dig) {
-                                       // Well not our problem then
-                               }
-                               // Clean and long dig
-                               else if (params.time > 2.0 && nocheat_t * 1.2 > params.time) {
-                                       // All is good, but grab time from pool; don't care if
-                                       // it's actually available
-                                       playersao->getDigPool().grab(params.time);
-                               }
-                               // Short or laggy dig
-                               // Try getting the time from pool
-                               else if (playersao->getDigPool().grab(params.time)) {
-                                       // All is good
-                               }
-                               // Dig not possible
-                               else {
-                                       infostream << "Server: NoCheat: " << player->getName()
-                                                       << " completed digging " << PP(p_under)
-                                                       << "too fast; not digging." << std::endl;
-                                       is_valid_dig = false;
-                                       // Call callbacks
-                                       m_script->on_cheat(playersao, "dug_too_fast");
-                               }
+               /* Cheat prevention */
+               bool is_valid_dig = true;
+               if (enable_anticheat && !isSingleplayer()) {
+                       v3s16 nocheat_p = playersao->getNoCheatDigPos();
+                       float nocheat_t = playersao->getNoCheatDigTime();
+                       playersao->noCheatDigEnd();
+                       // If player didn't start digging this, ignore dig
+                       if (nocheat_p != p_under) {
+                               infostream << "Server: " << player->getName()
+                                               << " started digging "
+                                               << PP(nocheat_p) << " and completed digging "
+                                               << PP(p_under) << "; not digging." << std::endl;
+                               is_valid_dig = false;
+                               // Call callbacks
+                               m_script->on_cheat(playersao, "finished_unknown_dig");
                        }
 
-                       /* Actually dig node */
-
-                       if (is_valid_dig && n.getContent() != CONTENT_IGNORE)
-                               m_script->node_on_dig(p_under, n, playersao);
-
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
-                       RemoteClient *client = getClient(pkt->getPeerId());
-                       // Send unusual result (that is, node not being removed)
-                       if (m_env->getMap().getNodeNoEx(p_under).getContent() != CONTENT_AIR) {
-                               // Re-send block to revert change on client-side
-                               client->SetBlockNotSent(blockpos);
+                       // Get player's wielded item
+                       // See also: Game::handleDigging
+                       ItemStack selected_item, main_item;
+                       playersao->getPlayer()->getWieldedItem(&selected_item, &main_item);
+
+                       // Get diggability and expected digging time
+                       DigParams params = getDigParams(m_nodedef->get(n).groups,
+                                       &selected_item.getToolCapabilities(m_itemdef),
+                                       selected_item.wear);
+                       // If can't dig, try hand
+                       if (!params.diggable) {
+                               params = getDigParams(m_nodedef->get(n).groups,
+                                       &main_item.getToolCapabilities(m_itemdef));
                        }
+                       // If can't dig, ignore dig
+                       if (!params.diggable) {
+                               infostream << "Server: " << player->getName()
+                                               << " completed digging " << PP(p_under)
+                                               << ", which is not diggable with tool; not digging."
+                                               << std::endl;
+                               is_valid_dig = false;
+                               // Call callbacks
+                               m_script->on_cheat(playersao, "dug_unbreakable");
+                       }
+                       // Check digging time
+                       // If already invalidated, we don't have to
+                       if (!is_valid_dig) {
+                               // Well not our problem then
+                       }
+                       // Clean and long dig
+                       else if (params.time > 2.0 && nocheat_t * 1.2 > params.time) {
+                               // All is good, but grab time from pool; don't care if
+                               // it's actually available
+                               playersao->getDigPool().grab(params.time);
+                       }
+                       // Short or laggy dig
+                       // Try getting the time from pool
+                       else if (playersao->getDigPool().grab(params.time)) {
+                               // All is good
+                       }
+                       // Dig not possible
                        else {
-                               client->ResendBlockIfOnWire(blockpos);
+                               infostream << "Server: " << player->getName()
+                                               << " completed digging " << PP(p_under)
+                                               << "too fast; not digging." << std::endl;
+                               is_valid_dig = false;
+                               // Call callbacks
+                               m_script->on_cheat(playersao, "dug_too_fast");
                        }
                }
-       } // action == 2
 
-       /*
-               3: place block or right-click object
-       */
-       else if (action == 3) {
-               ItemStack item = playersao->getWieldedItem();
+               /* Actually dig node */
+
+               if (is_valid_dig && n.getContent() != CONTENT_IGNORE)
+                       m_script->node_on_dig(p_under, n, playersao);
+
+               v3s16 blockpos = getNodeBlockPos(p_under);
+               RemoteClient *client = getClient(peer_id);
+               // Send unusual result (that is, node not being removed)
+               if (m_env->getMap().getNode(p_under).getContent() != CONTENT_AIR)
+                       // Re-send block to revert change on client-side
+                       client->SetBlockNotSent(blockpos);
+               else
+                       client->ResendBlockIfOnWire(blockpos);
+
+               return;
+       } // action == INTERACT_DIGGING_COMPLETED
+
+       // Place block or right-click object
+       case INTERACT_PLACE: {
+               Optional<ItemStack> main_item, offhand_item, place_item;
+               getWieldedItem(playersao, main_item);
+               bool use_offhand = getOffhandWieldedItem(playersao, offhand_item, place_item, m_itemdef, pointed);
 
                // Reset build time counter
                if (pointed.type == POINTEDTHING_NODE &&
-                               item.getDefinition(m_itemdef).type == ITEM_NODE)
-                       getClient(pkt->getPeerId())->m_time_from_building = 0.0;
+                               place_item->getDefinition(m_itemdef).type == ITEM_NODE)
+                       getClient(peer_id)->m_time_from_building = 0.0;
+
+               const bool had_prediction = !place_item->getDefinition(m_itemdef).
+                       node_placement_prediction.empty();
 
                if (pointed.type == POINTEDTHING_OBJECT) {
                        // Right click object
@@ -1318,80 +1254,86 @@ void Server::handleCommand_Interact(NetworkPacket* pkt)
                                        << pointed_object->getDescription() << std::endl;
 
                        // Do stuff
+                       if (m_script->item_OnSecondaryUse(use_offhand ? offhand_item : main_item, playersao, pointed)) {
+                               if (use_offhand
+                                               ? (offhand_item.has_value() && playersao->setOffhandWieldedItem(*offhand_item))
+                                               : (main_item.has_value() && playersao->setWieldedItem(*main_item)))
+                                       SendInventory(playersao, true);
+                       }
+
                        pointed_object->rightClick(playersao);
-               }
-               else if (m_script->item_OnPlace(
-                               item, playersao, pointed)) {
+               } else if (m_script->item_OnPlace(use_offhand ? offhand_item : main_item, playersao, pointed)) {
                        // Placement was handled in lua
 
                        // Apply returned ItemStack
-                       if (playersao->setWieldedItem(item)) {
-                               SendInventory(playersao);
-                       }
+                       if (use_offhand
+                                       ? (offhand_item.has_value() && playersao->setOffhandWieldedItem(*offhand_item))
+                                       : (main_item.has_value() && playersao->setWieldedItem(*main_item)))
+                               SendInventory(playersao, true);
                }
 
+               if (pointed.type != POINTEDTHING_NODE)
+                       return;
+
                // If item has node placement prediction, always send the
                // blocks to make sure the client knows what exactly happened
-               RemoteClient *client = getClient(pkt->getPeerId());
-               v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
-               v3s16 blockpos2 = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
-               if (!item.getDefinition(m_itemdef).node_placement_prediction.empty()) {
+               RemoteClient *client = getClient(peer_id);
+               v3s16 blockpos = getNodeBlockPos(pointed.node_abovesurface);
+               v3s16 blockpos2 = getNodeBlockPos(pointed.node_undersurface);
+               if (had_prediction) {
                        client->SetBlockNotSent(blockpos);
-                       if (blockpos2 != blockpos) {
+                       if (blockpos2 != blockpos)
                                client->SetBlockNotSent(blockpos2);
-                       }
-               }
-               else {
+               } else {
                        client->ResendBlockIfOnWire(blockpos);
-                       if (blockpos2 != blockpos) {
+                       if (blockpos2 != blockpos)
                                client->ResendBlockIfOnWire(blockpos2);
-                       }
                }
-       } // action == 3
 
-       /*
-               4: use
-       */
-       else if (action == 4) {
-               ItemStack item = playersao->getWieldedItem();
+               return;
+       } // action == INTERACT_PLACE
+
+       case INTERACT_USE: {
+               Optional<ItemStack> selected_item;
+               getWieldedItem(playersao, selected_item);
 
-               actionstream << player->getName() << " uses " << item.name
+               actionstream << player->getName() << " uses " << selected_item->name
                                << ", pointing at " << pointed.dump() << std::endl;
 
-               if (m_script->item_OnUse(
-                               item, playersao, pointed)) {
+               if (m_script->item_OnUse(selected_item, playersao, pointed)) {
                        // Apply returned ItemStack
-                       if (playersao->setWieldedItem(item)) {
-                               SendInventory(playersao);
-                       }
+                       if (selected_item.has_value() && playersao->setWieldedItem(*selected_item))
+                               SendInventory(playersao, true);
                }
 
-       } // action == 4
+               return;
+       }
 
-       /*
-               5: rightclick air
-       */
-       else if (action == 5) {
-               ItemStack item = playersao->getWieldedItem();
+       // Rightclick air
+       case INTERACT_ACTIVATE: {
+               Optional<ItemStack> main_item, offhand_item, place_item;
+               getWieldedItem(playersao, main_item);
+               bool use_offhand = getOffhandWieldedItem(playersao, offhand_item, place_item, m_itemdef, pointed);
 
                actionstream << player->getName() << " activates "
-                               << item.name << std::endl;
+                               << place_item->name << std::endl;
 
-               if (m_script->item_OnSecondaryUse(
-                               item, playersao)) {
-                       if( playersao->setWieldedItem(item)) {
-                               SendInventory(playersao);
-                       }
+               pointed.type = POINTEDTHING_NOTHING; // can only ever be NOTHING
+
+               if (m_script->item_OnSecondaryUse(use_offhand ? offhand_item : main_item, playersao, pointed)) {
+                       // Apply returned ItemStack
+                       if (use_offhand
+                                       ? (offhand_item.has_value() && playersao->setOffhandWieldedItem(*offhand_item))
+                                       : (main_item.has_value() && playersao->setWieldedItem(*main_item)))
+                               SendInventory(playersao, true);
                }
+
+               return;
        }
 
+       default:
+               warningstream << "Server: Invalid action " << action << std::endl;
 
-       /*
-               Catch invalid actions
-       */
-       else {
-               warningstream << "Server: Invalid action "
-                               << action << std::endl;
        }
 }
 
@@ -1431,22 +1373,23 @@ void Server::handleCommand_NodeMetaFields(NetworkPacket* pkt)
                fields[fieldname] = pkt->readLongString();
        }
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!"  << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -1482,22 +1425,23 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
                fields[fieldname] = pkt->readLongString();
        }
 
-       RemotePlayer *player = m_env->getPlayer(pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       RemotePlayer *player = m_env->getPlayer(peer_id);
 
        if (player == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
        PlayerSAO *playersao = player->getPlayerSAO();
        if (playersao == NULL) {
-               errorstream << "Server::ProcessData(): Canceling: "
-                               "No player object for peer_id=" << pkt->getPeerId()
-                               << " disconnecting peer!" << std::endl;
-               DisconnectPeer(pkt->getPeerId());
+               errorstream <<
+                       "Server::ProcessData(): Canceling: No player object for peer_id=" <<
+                       peer_id << " disconnecting peer!" << std::endl;
+               DisconnectPeer(peer_id);
                return;
        }
 
@@ -1507,11 +1451,12 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
        }
 
        // verify that we displayed the formspec to the user
-       const auto peer_state_iterator = m_formspec_state_data.find(pkt->getPeerId());
+       const auto peer_state_iterator = m_formspec_state_data.find(peer_id);
        if (peer_state_iterator != m_formspec_state_data.end()) {
                const std::string &server_formspec_name = peer_state_iterator->second;
                if (client_formspec_name == server_formspec_name) {
-                       if (fields["quit"] == "true")
+                       auto it = fields.find("quit");
+                       if (it != fields.end() && it->second == "true")
                                m_formspec_state_data.erase(peer_state_iterator);
 
                        m_script->on_playerReceiveFields(playersao, client_formspec_name, fields);
@@ -1532,15 +1477,14 @@ void Server::handleCommand_InventoryFields(NetworkPacket* pkt)
 
 void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
 {
-       RemoteClient* client = getClient(pkt->getPeerId(), CS_Invalid);
+       session_t peer_id = pkt->getPeerId();
+       RemoteClient *client = getClient(peer_id, CS_Invalid);
        ClientState cstate = client->getState();
+       const std::string playername = client->getName();
 
-       std::string playername = client->getName();
-
-       std::string salt;
-       std::string verification_key;
+       std::string salt, verification_key;
 
-       std::string addr_s = getPeerAddress(pkt->getPeerId()).serializeString();
+       std::string addr_s = getPeerAddress(peer_id).serializeString();
        u8 is_empty;
 
        *pkt >> salt >> verification_key >> is_empty;
@@ -1548,31 +1492,44 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
        verbosestream << "Server: Got TOSERVER_FIRST_SRP from " << addr_s
                << ", with is_empty=" << (is_empty == 1) << std::endl;
 
+       const bool empty_disallowed = !isSingleplayer() && is_empty == 1 &&
+               g_settings->getBool("disallow_empty_password");
+
        // Either this packet is sent because the user is new or to change the password
        if (cstate == CS_HelloSent) {
                if (!client->isMechAllowed(AUTH_MECHANISM_FIRST_SRP)) {
                        actionstream << "Server: Client from " << addr_s
                                        << " tried to set password without being "
                                        << "authenticated, or the username being new." << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                        return;
                }
 
-               if (!isSingleplayer() &&
-                               g_settings->getBool("disallow_empty_password") &&
-                               is_empty == 1) {
+               if (empty_disallowed) {
                        actionstream << "Server: " << playername
                                        << " supplied empty password from " << addr_s << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_EMPTY_PASSWORD);
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_EMPTY_PASSWORD);
                        return;
                }
 
                std::string initial_ver_key;
-
                initial_ver_key = encode_srp_verifier(verification_key, salt);
+
+               // It is possible for multiple connections to get this far with the same
+               // player name. In the end only one player with a given name will be emerged
+               // (see Server::StateTwoClientInit) but we still have to be careful here.
+               if (m_script->getAuth(playername, nullptr, nullptr)) {
+                       // Another client beat us to it
+                       actionstream << "Server: Client from " << addr_s
+                               << " tried to register " << playername << " a second time."
+                               << std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_ALREADY_CONNECTED);
+                       return;
+               }
                m_script->createAuth(playername, initial_ver_key);
+               m_script->on_authplayer(playername, addr_s, true);
 
-               acceptAuth(pkt->getPeerId(), false);
+               acceptAuth(peer_id, false);
        } else {
                if (cstate < CS_SudoMode) {
                        infostream << "Server::ProcessData(): Ignoring TOSERVER_FIRST_SRP from "
@@ -1580,48 +1537,57 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
                                        << std::endl;
                        return;
                }
-               m_clients.event(pkt->getPeerId(), CSE_SudoLeave);
+               m_clients.event(peer_id, CSE_SudoLeave);
+
+               if (empty_disallowed) {
+                       actionstream << "Server: " << playername
+                                       << " supplied empty password" << std::endl;
+                       SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                               L"Changing to an empty password is not allowed."));
+                       return;
+               }
+
                std::string pw_db_field = encode_srp_verifier(verification_key, salt);
                bool success = m_script->setPassword(playername, pw_db_field);
                if (success) {
                        actionstream << playername << " changes password" << std::endl;
-                       SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                                       L"Password change successful."));
+                       SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                               L"Password change successful."));
                } else {
-                       actionstream << playername << " tries to change password but "
-                               << "it fails" << std::endl;
-                       SendChatMessage(pkt->getPeerId(), ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
-                                       L"Password change failed or unavailable."));
+                       actionstream << playername <<
+                               " tries to change password but it fails" << std::endl;
+                       SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                               L"Password change failed or unavailable."));
                }
        }
 }
 
 void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
 {
-       RemoteClient* client = getClient(pkt->getPeerId(), CS_Invalid);
+       session_t peer_id = pkt->getPeerId();
+       RemoteClient *client = getClient(peer_id, CS_Invalid);
        ClientState cstate = client->getState();
 
-       bool wantSudo = (cstate == CS_Active);
-
        if (!((cstate == CS_HelloSent) || (cstate == CS_Active))) {
-               actionstream << "Server: got SRP _A packet in wrong state "
-                       << cstate << " from "
-                       << getPeerAddress(pkt->getPeerId()).serializeString()
-                       << ". Ignoring." << std::endl;
+               actionstream << "Server: got SRP _A packet in wrong state " << cstate <<
+                       " from " << getPeerAddress(peer_id).serializeString() <<
+                       ". Ignoring." << std::endl;
                return;
        }
 
+       const bool wantSudo = (cstate == CS_Active);
+
        if (client->chosen_mech != AUTH_MECHANISM_NONE) {
-               actionstream << "Server: got SRP _A packet, while auth"
-                       << "is already going on with mech " << client->chosen_mech
-                       << " from " << getPeerAddress(pkt->getPeerId()).serializeString()
-                       << " (wantSudo=" << wantSudo << "). Ignoring." << std::endl;
+               actionstream << "Server: got SRP _A packet, while auth is already "
+                       "going on with mech " << client->chosen_mech << " from " <<
+                       getPeerAddress(peer_id).serializeString() <<
+                       " (wantSudo=" << wantSudo << "). Ignoring." << std::endl;
                if (wantSudo) {
-                       DenySudoAccess(pkt->getPeerId());
+                       DenySudoAccess(peer_id);
                        return;
                }
 
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                return;
        }
 
@@ -1638,27 +1604,26 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
 
        if (wantSudo) {
                if (!client->isSudoMechAllowed(chosen)) {
-                       actionstream << "Server: Player \"" << client->getName()
-                               << "\" at " << getPeerAddress(pkt->getPeerId()).serializeString()
-                               << " tried to change password using unallowed mech "
-                               << chosen << "." << std::endl;
-                       DenySudoAccess(pkt->getPeerId());
+                       actionstream << "Server: Player \"" << client->getName() <<
+                               "\" at " << getPeerAddress(peer_id).serializeString() <<
+                               " tried to change password using unallowed mech " << chosen <<
+                               "." << std::endl;
+                       DenySudoAccess(peer_id);
                        return;
                }
        } else {
                if (!client->isMechAllowed(chosen)) {
-                       actionstream << "Server: Client tried to authenticate from "
-                               << getPeerAddress(pkt->getPeerId()).serializeString()
-                               << " using unallowed mech " << chosen << "." << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+                       actionstream << "Server: Client tried to authenticate from " <<
+                               getPeerAddress(peer_id).serializeString() <<
+                               " using unallowed mech " << chosen << "." << std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                        return;
                }
        }
 
        client->chosen_mech = chosen;
 
-       std::string salt;
-       std::string verifier;
+       std::string salt, verifier;
 
        if (based_on == 0) {
 
@@ -1666,10 +1631,10 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
                        &verifier, &salt);
        } else if (!decode_srp_verifier_and_salt(client->enc_pwd, &verifier, &salt)) {
                // Non-base64 errors should have been catched in the init handler
-               actionstream << "Server: User " << client->getName()
-                       << " tried to log in, but srp verifier field"
-                       << " was invalid (most likely invalid base64)." << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_SERVER_FAIL);
+               actionstream << "Server: User " << client->getName() <<
+                       " tried to log in, but srp verifier field was invalid (most likely "
+                       "invalid base64)." << std::endl;
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_SERVER_FAIL);
                return;
        }
 
@@ -1689,48 +1654,49 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
                        << " tried to log in, SRP-6a safety check violated in _A handler."
                        << std::endl;
                if (wantSudo) {
-                       DenySudoAccess(pkt->getPeerId());
+                       DenySudoAccess(peer_id);
+                       client->resetChosenMech();
                        return;
                }
 
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                return;
        }
 
-       NetworkPacket resp_pkt(TOCLIENT_SRP_BYTES_S_B, 0, pkt->getPeerId());
+       NetworkPacket resp_pkt(TOCLIENT_SRP_BYTES_S_B, 0, peer_id);
        resp_pkt << salt << std::string(bytes_B, len_B);
        Send(&resp_pkt);
 }
 
 void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
 {
-       RemoteClient* client = getClient(pkt->getPeerId(), CS_Invalid);
+       session_t peer_id = pkt->getPeerId();
+       RemoteClient *client = getClient(peer_id, CS_Invalid);
        ClientState cstate = client->getState();
+       const std::string addr_s = client->getAddress().serializeString();
+       const std::string playername = client->getName();
 
-       bool wantSudo = (cstate == CS_Active);
+       const bool wantSudo = (cstate == CS_Active);
 
-       verbosestream << "Server: Received TOCLIENT_SRP_BYTES_M." << std::endl;
+       verbosestream << "Server: Received TOSERVER_SRP_BYTES_M." << std::endl;
 
        if (!((cstate == CS_HelloSent) || (cstate == CS_Active))) {
-               actionstream << "Server: got SRP _M packet in wrong state "
-                       << cstate << " from "
-                       << getPeerAddress(pkt->getPeerId()).serializeString()
-                       << ". Ignoring." << std::endl;
+               warningstream << "Server: got SRP_M packet in wrong state "
+                       << cstate << " from " << addr_s << ". Ignoring." << std::endl;
                return;
        }
 
        if (client->chosen_mech != AUTH_MECHANISM_SRP &&
                        client->chosen_mech != AUTH_MECHANISM_LEGACY_PASSWORD) {
-               actionstream << "Server: got SRP _M packet, while auth"
-                       << "is going on with mech " << client->chosen_mech
-                       << " from " << getPeerAddress(pkt->getPeerId()).serializeString()
-                       << " (wantSudo=" << wantSudo << "). Denying." << std::endl;
+               warningstream << "Server: got SRP_M packet, while auth "
+                       "is going on with mech " << client->chosen_mech << " from "
+                       << addr_s << " (wantSudo=" << wantSudo << "). Denying." << std::endl;
                if (wantSudo) {
-                       DenySudoAccess(pkt->getPeerId());
+                       DenySudoAccess(peer_id);
                        return;
                }
 
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                return;
        }
 
@@ -1739,10 +1705,9 @@ void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
 
        if (srp_verifier_get_session_key_length((SRPVerifier *) client->auth_data)
                        != bytes_M.size()) {
-               actionstream << "Server: User " << client->getName()
-                       << " at " << getPeerAddress(pkt->getPeerId()).serializeString()
+               actionstream << "Server: User " << playername << " at " << addr_s
                        << " sent bytes_M with invalid length " << bytes_M.size() << std::endl;
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_UNEXPECTED_DATA);
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                return;
        }
 
@@ -1753,39 +1718,36 @@ void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
 
        if (!bytes_HAMK) {
                if (wantSudo) {
-                       actionstream << "Server: User " << client->getName()
-                               << " at " << getPeerAddress(pkt->getPeerId()).serializeString()
+                       actionstream << "Server: User " << playername << " at " << addr_s
                                << " tried to change their password, but supplied wrong"
                                << " (SRP) password for authentication." << std::endl;
-                       DenySudoAccess(pkt->getPeerId());
+                       DenySudoAccess(peer_id);
+                       client->resetChosenMech();
                        return;
                }
 
-               std::string ip = getPeerAddress(pkt->getPeerId()).serializeString();
-               actionstream << "Server: User " << client->getName()
-                       << " at " << ip
-                       << " supplied wrong password (auth mechanism: SRP)."
-                       << std::endl;
-               m_script->on_auth_failure(client->getName(), ip);
-               DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_WRONG_PASSWORD);
+               actionstream << "Server: User " << playername << " at " << addr_s
+                       << " supplied wrong password (auth mechanism: SRP)." << std::endl;
+               m_script->on_authplayer(playername, addr_s, false);
+               DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_PASSWORD);
                return;
        }
 
        if (client->create_player_on_auth_success) {
-               std::string playername = client->getName();
                m_script->createAuth(playername, client->enc_pwd);
 
-               std::string checkpwd; // not used, but needed for passing something
-               if (!m_script->getAuth(playername, &checkpwd, NULL)) {
-                       actionstream << "Server: " << playername << " cannot be authenticated"
-                               << " (auth handler does not work?)" << std::endl;
-                       DenyAccess(pkt->getPeerId(), SERVER_ACCESSDENIED_SERVER_FAIL);
+               if (!m_script->getAuth(playername, nullptr, nullptr)) {
+                       errorstream << "Server: " << playername <<
+                               " cannot be authenticated (auth handler does not work?)" <<
+                               std::endl;
+                       DenyAccess(peer_id, SERVER_ACCESSDENIED_SERVER_FAIL);
                        return;
                }
                client->create_player_on_auth_success = false;
        }
 
-       acceptAuth(pkt->getPeerId(), wantSudo);
+       m_script->on_authplayer(playername, addr_s, true);
+       acceptAuth(peer_id, wantSudo);
 }
 
 /*
@@ -1797,20 +1759,21 @@ void Server::handleCommand_ModChannelJoin(NetworkPacket *pkt)
        std::string channel_name;
        *pkt >> channel_name;
 
-       NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL, 1 + 2 + channel_name.size(),
-               pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL,
+               1 + 2 + channel_name.size(), peer_id);
 
        // Send signal to client to notify join succeed or not
        if (g_settings->getBool("enable_mod_channels") &&
-                       m_modchannel_mgr->joinChannel(channel_name, pkt->getPeerId())) {
+                       m_modchannel_mgr->joinChannel(channel_name, peer_id)) {
                resp_pkt << (u8) MODCHANNEL_SIGNAL_JOIN_OK;
-               infostream << "Peer " << pkt->getPeerId() << " joined channel " << channel_name
-                               << std::endl;
+               infostream << "Peer " << peer_id << " joined channel " <<
+                       channel_name << std::endl;
        }
        else {
                resp_pkt << (u8)MODCHANNEL_SIGNAL_JOIN_FAILURE;
-               infostream << "Peer " << pkt->getPeerId() << " tried to join channel "
-                       << channel_name << ", but was already registered." << std::endl;
+               infostream << "Peer " << peer_id << " tried to join channel " <<
+                       channel_name << ", but was already registered." << std::endl;
        }
        resp_pkt << channel_name;
        Send(&resp_pkt);
@@ -1821,19 +1784,20 @@ void Server::handleCommand_ModChannelLeave(NetworkPacket *pkt)
        std::string channel_name;
        *pkt >> channel_name;
 
-       NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL, 1 + 2 + channel_name.size(),
-               pkt->getPeerId());
+       session_t peer_id = pkt->getPeerId();
+       NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL,
+               1 + 2 + channel_name.size(), peer_id);
 
        // Send signal to client to notify join succeed or not
        if (g_settings->getBool("enable_mod_channels") &&
-                       m_modchannel_mgr->leaveChannel(channel_name, pkt->getPeerId())) {
+                       m_modchannel_mgr->leaveChannel(channel_name, peer_id)) {
                resp_pkt << (u8)MODCHANNEL_SIGNAL_LEAVE_OK;
-               infostream << "Peer " << pkt->getPeerId() << " left channel " << channel_name
-                               << std::endl;
+               infostream << "Peer " << peer_id << " left channel " << channel_name <<
+                       std::endl;
        } else {
                resp_pkt << (u8) MODCHANNEL_SIGNAL_LEAVE_FAILURE;
-               infostream << "Peer " << pkt->getPeerId() << " left channel " << channel_name
-                               << ", but was not registered." << std::endl;
+               infostream << "Peer " << peer_id << " left channel " << channel_name <<
+                       ", but was not registered." << std::endl;
        }
        resp_pkt << channel_name;
        Send(&resp_pkt);
@@ -1844,8 +1808,10 @@ void Server::handleCommand_ModChannelMsg(NetworkPacket *pkt)
        std::string channel_name, channel_msg;
        *pkt >> channel_name >> channel_msg;
 
-       verbosestream << "Mod channel message received from peer " << pkt->getPeerId()
-                       << " on channel " << channel_name << " message: " << channel_msg << std::endl;
+       session_t peer_id = pkt->getPeerId();
+       verbosestream << "Mod channel message received from peer " << peer_id <<
+               " on channel " << channel_name << " message: " << channel_msg <<
+               std::endl;
 
        // If mod channels are not enabled, discard message
        if (!g_settings->getBool("enable_mod_channels")) {
@@ -1854,8 +1820,8 @@ void Server::handleCommand_ModChannelMsg(NetworkPacket *pkt)
 
        // If channel not registered, signal it and ignore message
        if (!m_modchannel_mgr->channelRegistered(channel_name)) {
-               NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL, 1 + 2 + channel_name.size(),
-                       pkt->getPeerId());
+               NetworkPacket resp_pkt(TOCLIENT_MODCHANNEL_SIGNAL,
+                       1 + 2 + channel_name.size(), peer_id);
                resp_pkt << (u8)MODCHANNEL_SIGNAL_CHANNEL_NOT_REGISTERED << channel_name;
                Send(&resp_pkt);
                return;
@@ -1863,5 +1829,47 @@ void Server::handleCommand_ModChannelMsg(NetworkPacket *pkt)
 
        // @TODO: filter, rate limit
 
-       broadcastModChannelMessage(channel_name, channel_msg, pkt->getPeerId());
+       broadcastModChannelMessage(channel_name, channel_msg, peer_id);
+}
+
+void Server::handleCommand_HaveMedia(NetworkPacket *pkt)
+{
+       std::vector<u32> tokens;
+       u8 numtokens;
+
+       *pkt >> numtokens;
+       for (u16 i = 0; i < numtokens; i++) {
+               u32 n;
+               *pkt >> n;
+               tokens.emplace_back(n);
+       }
+
+       const session_t peer_id = pkt->getPeerId();
+       auto player = m_env->getPlayer(peer_id);
+
+       for (const u32 token : tokens) {
+               auto it = m_pending_dyn_media.find(token);
+               if (it == m_pending_dyn_media.end())
+                       continue;
+               if (it->second.waiting_players.count(peer_id)) {
+                       it->second.waiting_players.erase(peer_id);
+                       if (player)
+                               getScriptIface()->on_dynamic_media_added(token, player->getName());
+               }
+       }
+}
+
+void Server::handleCommand_UpdateClientInfo(NetworkPacket *pkt)
+{
+       ClientDynamicInfo info;
+       *pkt >> info.render_target_size.X;
+       *pkt >> info.render_target_size.Y;
+       *pkt >> info.real_gui_scaling;
+       *pkt >> info.real_hud_scaling;
+       *pkt >> info.max_fs_size.X;
+       *pkt >> info.max_fs_size.Y;
+
+       session_t peer_id = pkt->getPeerId();
+       RemoteClient *client = getClient(peer_id, CS_Invalid);
+       client->setDynamicInfo(info);
 }