]> git.lizzy.rs Git - minetest.git/blobdiff - src/network/serverpackethandler.cpp
Dual wielding
[minetest.git] / src / network / serverpackethandler.cpp
index 5136eb0ec58b66441d402616e6fab949a4b5bef4..1e49538dc863a9babddd44952e9d3209b46b3fe3 100644 (file)
@@ -34,11 +34,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #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)
 {
@@ -55,12 +57,12 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
        session_t peer_id = pkt->getPeerId();
        RemoteClient *client = getClient(peer_id, CS_Created);
 
+       Address addr;
        std::string addr_s;
        try {
-               Address address = getPeerAddress(peer_id);
-               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
@@ -72,19 +74,20 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                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=" << peer_id << ")" << std::endl;
                return;
        }
 
+       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) {
+       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);
@@ -106,14 +109,14 @@ 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 << " client_max=" << (int)client_max << std::endl;
+                       addr_s << " ser_fmt_max=" << (int)client_max << std::endl;
                DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_VERSION);
                return;
        }
@@ -148,7 +151,7 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                        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;
+                       addr_s << " proto_max=" << (int)max_net_proto_version << std::endl;
                DenyAccess(peer_id, SERVER_ACCESSDENIED_WRONG_VERSION);
                return;
        }
@@ -174,6 +177,16 @@ void Server::handleCommand_Init(NetworkPacket* pkt)
                return;
        }
 
+       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
 
@@ -217,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;
@@ -317,7 +330,7 @@ void Server::handleCommand_Init2(NetworkPacket* pkt)
        // Send active objects
        {
                PlayerSAO *sao = getPlayerSAO(peer_id);
-               if (client && sao)
+               if (sao)
                        SendActiveObjectRemoveAdd(client, sao);
        }
 
@@ -352,16 +365,15 @@ void Server::handleCommand_RequestMedia(NetworkPacket* pkt)
        session_t peer_id = pkt->getPeerId();
        infostream << "Sending " << numfiles << " files to " <<
                getPlayerName(peer_id) << std::endl;
-       verbosestream << "TOSERVER_REQUEST_MEDIA: " << 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(peer_id, tosend);
@@ -371,52 +383,47 @@ void Server::handleCommand_ClientReady(NetworkPacket* pkt)
 {
        session_t peer_id = pkt->getPeerId();
 
-       PlayerSAO* playersao = StageTwoClientInit(peer_id);
-
-       if (playersao == NULL) {
-               errorstream << "TOSERVER_CLIENT_READY stage 2 client init failed "
-                       "peer_id=" << peer_id << std::endl;
-               DisconnectPeer(peer_id);
-               return;
-       }
-
-
-       if (pkt->getSize() < 8) {
-               errorstream << "TOSERVER_CLIENT_READY client sent inconsistent data, "
-                       "disconnecting peer_id: " << peer_id << std::endl;
-               DisconnectPeer(peer_id);
-               return;
-       }
-
+       // 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;
+
        *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->getRemainingBytes() >= 2)
-               *pkt >> playersao->getPlayer()->formspec_version;
+       // 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;
+       }
+
+       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()) {
+       if (m_shutdown_state.isTimerRunning())
                SendChatMessage(peer_id, m_shutdown_state.getShutdownTimerMessage());
-       }
 }
 
 void Server::handleCommand_GotBlocks(NetworkPacket* pkt)
@@ -435,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;
@@ -468,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;
 
@@ -483,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);
+       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
@@ -596,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"
@@ -613,38 +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);
+               m_inventory_mgr->setInventoryModified(ma->from_inv);
                if (ma->from_inv != ma->to_inv)
-                       setInventoryModified(ma->to_inv);
+                       m_inventory_mgr->setInventoryModified(ma->to_inv);
 
-               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());
-
-               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()->getEyePosition();
-                       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
@@ -654,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;
                }
 
@@ -666,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;
                }
        }
@@ -685,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);
+               m_inventory_mgr->setInventoryModified(da->from_inv);
 
                /*
                        Disable dropping items out of craftpreview
@@ -698,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;
                }
        }
@@ -721,48 +725,34 @@ 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);
-
-               //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;
+       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;
 
        session_t peer_id = pkt->getPeerId();
        RemotePlayer *player = m_env->getPlayer(peer_id);
@@ -774,15 +764,13 @@ void Server::handleCommand_ChatMessage(NetworkPacket* pkt)
                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(peer_id, ChatMessage(CHATMESSAGE_TYPE_NORMAL,
-                       answer_to_sender, wname));
+               SendChatMessage(peer_id, ChatMessage(CHATMESSAGE_TYPE_SYSTEM,
+                       answer_to_sender));
        }
 }
 
@@ -825,8 +813,7 @@ void Server::handleCommand_Damage(NetworkPacket* pkt)
                                << std::endl;
 
                PlayerHPChangeReason reason(PlayerHPChangeReason::FALL);
-               playersao->setHP((s32)playersao->getHP() - (s32)damage, reason);
-               SendPlayerHPOrDie(playersao, reason);
+               playersao->setHP((s32)playersao->getHP() - (s32)damage, reason, true);
        }
 }
 
@@ -859,6 +846,15 @@ void Server::handleCommand_PlayerItem(NetworkPacket* pkt)
 
        *pkt >> 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);
 }
 
@@ -891,10 +887,10 @@ void Server::handleCommand_Respawn(NetworkPacket* pkt)
 
 bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std::string &what)
 {
-       ItemStack selected_item, hand_item;
-       player->getWieldedItem(&selected_item, &hand_item);
+       ItemStack selected_item, main_item;
+       player->getWieldedItem(&selected_item, &main_item);
        f32 max_d = BS * getToolRange(selected_item.getDefinition(m_itemdef),
-                       hand_item.getDefinition(m_itemdef));
+                       main_item.getDefinition(m_itemdef));
 
        // Cube diagonal * 1.5 for maximal supported node extents:
        // sqrt(3) * 1.5 â‰… 2.6
@@ -902,8 +898,8 @@ bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std:
                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(player->getPlayerSAO(), "interacted_too_far");
                return false;
@@ -911,6 +907,21 @@ bool Server::checkInteractDistance(RemotePlayer *player, const f32 d, const std:
        return true;
 }
 
+// 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)
 {
        /*
@@ -956,7 +967,7 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
        }
 
        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
@@ -974,11 +985,17 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
        v3f player_pos = playersao->getLastGoodPosition();
 
        // Update wielded item
-       playersao->getPlayer()->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;
@@ -992,17 +1009,6 @@ 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
        */
@@ -1010,16 +1016,19 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
                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(peer_id);
                // Digging completed -> under
                if (action == INTERACT_DIGGING_COMPLETED) {
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
+                       v3s16 blockpos = getNodeBlockPos(pointed.node_undersurface);
                        client->SetBlockNotSent(blockpos);
                }
                // Placement -> above
                else if (action == INTERACT_PLACE) {
-                       v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
+                       v3s16 blockpos = getNodeBlockPos(pointed.node_abovesurface);
                        client->SetBlockNotSent(blockpos);
                }
                return;
@@ -1027,7 +1036,6 @@ 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");
@@ -1035,13 +1043,27 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
        if ((action == INTERACT_START_DIGGING || action == INTERACT_DIGGING_COMPLETED ||
                        action == INTERACT_PLACE || action == INTERACT_USE) &&
                        enable_anticheat && !isSingleplayer()) {
-               float d = playersao->getEyePosition().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(peer_id);
-                       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;
                }
        }
@@ -1052,20 +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 == INTERACT_START_DIGGING) {
+       switch (action) {
+       // Start digging or punch object
+       case INTERACT_START_DIGGING: {
                if (pointed.type == POINTEDTHING_NODE) {
                        MapNode n(CONTENT_IGNORE);
                        bool 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(peer_id, getNodeBlockPos(p_above),
-                                       false);
+                               m_emerge->enqueueBlockEmerge(peer_id,
+                                       getNodeBlockPos(pointed.node_abovesurface), false);
                        }
 
                        if (n.getContent() != CONTENT_IGNORE)
@@ -1073,167 +1095,153 @@ 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;
 
-                       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();
-
-                       u16 src_original_hp = pointed_object->getHP();
-                       u16 dst_origin_hp = playersao->getHP();
-
-                       u16 wear = pointed_object->punch(dir, &toolcap, playersao,
-                                       time_from_last_punch);
-
-                       // 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);
-
-                       // 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);
 
+               return;
        } // action == INTERACT_START_DIGGING
 
-       /*
-               1: stop digging
-       */
-       else if (action == INTERACT_STOP_DIGGING) {
-       } // action == INTERACT_STOP_DIGGING
+       case INTERACT_STOP_DIGGING:
+               // Nothing to do
+               return;
 
-       /*
-               2: Digging completed
-       */
-       else if (action == INTERACT_DIGGING_COMPLETED) {
+       case INTERACT_DIGGING_COMPLETED: {
                // Only digging of nodes
-               if (pointed.type == POINTEDTHING_NODE) {
-                       bool pos_ok;
-                       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(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
-                               // See also: Game::handleDigging
-                               ItemStack selected_item, hand_item;
-                               playersao->getPlayer()->getWieldedItem(&selected_item, &hand_item);
-
-                               // Get diggability and expected digging time
-                               DigParams params = getDigParams(m_nodedef->get(n).groups,
-                                               &selected_item.getToolCapabilities(m_itemdef));
-                               // If can't dig, try hand
-                               if (!params.diggable) {
-                                       params = getDigParams(m_nodedef->get(n).groups,
-                                               &hand_item.getToolCapabilities(m_itemdef));
-                               }
-                               // 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(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);
+                       // 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");
                        }
                }
+
+               /* 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
 
-       /*
-               3: place block or right-click object
-       */
-       else if (action == INTERACT_PLACE) {
-               ItemStack selected_item;
-               playersao->getWieldedItem(&selected_item, nullptr);
+       // 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 &&
-                               selected_item.getDefinition(m_itemdef).type == ITEM_NODE)
+                               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
 
@@ -1246,90 +1254,86 @@ void Server::handleCommand_Interact(NetworkPacket *pkt)
                                        << pointed_object->getDescription() << std::endl;
 
                        // Do stuff
-                       if (m_script->item_OnSecondaryUse(
-                                       selected_item, playersao, pointed)) {
-                               if (playersao->setWieldedItem(selected_item)) {
+                       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(
-                               selected_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(selected_item)) {
+                       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(peer_id);
-               v3s16 blockpos = getNodeBlockPos(floatToInt(pointed_pos_above, BS));
-               v3s16 blockpos2 = getNodeBlockPos(floatToInt(pointed_pos_under, BS));
-               if (!selected_item.getDefinition(m_itemdef).node_placement_prediction.empty()) {
+               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);
-                       }
                }
+
+               return;
        } // action == INTERACT_PLACE
 
-       /*
-               4: use
-       */
-       else if (action == INTERACT_USE) {
-               ItemStack selected_item;
-               playersao->getWieldedItem(&selected_item, nullptr);
+       case INTERACT_USE: {
+               Optional<ItemStack> selected_item;
+               getWieldedItem(playersao, selected_item);
 
-               actionstream << player->getName() << " uses " << selected_item.name
+               actionstream << player->getName() << " uses " << selected_item->name
                                << ", pointing at " << pointed.dump() << std::endl;
 
-               if (m_script->item_OnUse(
-                               selected_item, playersao, pointed)) {
+               if (m_script->item_OnUse(selected_item, playersao, pointed)) {
                        // Apply returned ItemStack
-                       if (playersao->setWieldedItem(selected_item)) {
+                       if (selected_item.has_value() && playersao->setWieldedItem(*selected_item))
                                SendInventory(playersao, true);
-                       }
                }
 
-       } // action == INTERACT_USE
+               return;
+       }
 
-       /*
-               5: rightclick air
-       */
-       else if (action == INTERACT_ACTIVATE) {
-               ItemStack selected_item;
-               playersao->getWieldedItem(&selected_item, nullptr);
+       // 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 "
-                               << selected_item.name << std::endl;
+                               << place_item->name << std::endl;
 
                pointed.type = POINTEDTHING_NOTHING; // can only ever be NOTHING
 
-               if (m_script->item_OnSecondaryUse(
-                               selected_item, playersao, pointed)) {
-                       if (playersao->setWieldedItem(selected_item)) {
+               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);
-                       }
                }
-       } // action == INTERACT_ACTIVATE
 
+               return;
+       }
+
+       default:
+               warningstream << "Server: Invalid action " << action << std::endl;
 
-       /*
-               Catch invalid actions
-       */
-       else {
-               warningstream << "Server: Invalid action "
-                               << action << std::endl;
        }
 }
 
@@ -1476,11 +1480,9 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
        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(peer_id).serializeString();
        u8 is_empty;
@@ -1490,6 +1492,9 @@ 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)) {
@@ -1500,9 +1505,7 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
                        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(peer_id, SERVER_ACCESSDENIED_EMPTY_PASSWORD);
@@ -1510,9 +1513,21 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
                }
 
                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(peer_id, false);
        } else {
@@ -1523,6 +1538,15 @@ void Server::handleCommand_FirstSrp(NetworkPacket* pkt)
                        return;
                }
                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) {
@@ -1544,8 +1568,6 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
        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(peer_id).serializeString() <<
@@ -1553,6 +1575,8 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
                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 " <<
@@ -1599,8 +1623,7 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
 
        client->chosen_mech = chosen;
 
-       std::string salt;
-       std::string verifier;
+       std::string salt, verifier;
 
        if (based_on == 0) {
 
@@ -1632,6 +1655,7 @@ void Server::handleCommand_SrpBytesA(NetworkPacket* pkt)
                        << std::endl;
                if (wantSudo) {
                        DenySudoAccess(peer_id);
+                       client->resetChosenMech();
                        return;
                }
 
@@ -1649,24 +1673,24 @@ void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
        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(peer_id).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(peer_id).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(peer_id);
                        return;
@@ -1681,9 +1705,8 @@ 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(peer_id).serializeString() <<
-                       " sent bytes_M with invalid length " << bytes_M.size() << std::endl;
+               actionstream << "Server: User " << playername << " at " << addr_s
+                       << " sent bytes_M with invalid length " << bytes_M.size() << std::endl;
                DenyAccess(peer_id, SERVER_ACCESSDENIED_UNEXPECTED_DATA);
                return;
        }
@@ -1695,29 +1718,26 @@ void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
 
        if (!bytes_HAMK) {
                if (wantSudo) {
-                       actionstream << "Server: User " << client->getName() << " at " <<
-                               getPeerAddress(peer_id).serializeString() <<
-                               " tried to change their password, but supplied wrong (SRP) "
-                               "password for authentication." << std::endl;
+                       actionstream << "Server: User " << playername << " at " << addr_s
+                               << " tried to change their password, but supplied wrong"
+                               << " (SRP) password for authentication." << std::endl;
                        DenySudoAccess(peer_id);
+                       client->resetChosenMech();
                        return;
                }
 
-               std::string ip = getPeerAddress(peer_id).serializeString();
-               actionstream << "Server: User " << client->getName() << " at " << ip <<
-                       " supplied wrong password (auth mechanism: SRP)." << std::endl;
-               m_script->on_auth_failure(client->getName(), ip);
+               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 <<
+               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);
@@ -1726,6 +1746,7 @@ void Server::handleCommand_SrpBytesM(NetworkPacket* pkt)
                client->create_player_on_auth_success = false;
        }
 
+       m_script->on_authplayer(playername, addr_s, true);
        acceptAuth(peer_id, wantSudo);
 }
 
@@ -1810,3 +1831,45 @@ void Server::handleCommand_ModChannelMsg(NetworkPacket *pkt)
 
        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);
+}