From 4242431407459e91f725de9cb7fb1e2ffade216d Mon Sep 17 00:00:00 2001 From: 9caihezi <9caihezi@gmail.com> Date: Tue, 9 Jan 2018 00:37:10 +1100 Subject: Rewrite cClientHandle::HandleRightClick (#4089) * Add hand parameter to distinguish main hand/off hand. * Add a new function cClientHandle::HandleUseItem to separate the functionality of using an item without a target block. This matches the protocol with client version >= 1.9 * Always actively update the status of a block if the placement fails (by out of reach or rejected by plugin). * Do not call plugin callback CallHookPlayerRightClick(-1, 255, -1, -1, 0, 0, 0) when using item. The CallHookPlayerUsingItem will still be called. Now at most one of CallHookPlayerRightClick, CallHookPlayerUsingBlock, CallHookPlayerUsingItem and CallHookPlayerEating will be called based on the type of action (not including the used version of callbacks). * Do not count using item as BlockInteractionsRate check (Using item takes time). * Now we can open chests(etc.) when sneaking as long as the player's hand is empty. This is what vanilla server does. --- src/ClientHandle.cpp | 278 ++++++++++++++++++++++++--------------------------- 1 file changed, 130 insertions(+), 148 deletions(-) (limited to 'src/ClientHandle.cpp') diff --git a/src/ClientHandle.cpp b/src/ClientHandle.cpp index 8470e7de6..3a07de7f1 100644 --- a/src/ClientHandle.cpp +++ b/src/ClientHandle.cpp @@ -1395,186 +1395,113 @@ void cClientHandle::FinishDigAnimation() -void cClientHandle::HandleRightClick(int a_BlockX, int a_BlockY, int a_BlockZ, eBlockFace a_BlockFace, int a_CursorX, int a_CursorY, int a_CursorZ, const cItem & a_HeldItem) -{ - // TODO: Rewrite this function - +void cClientHandle::HandleRightClick(int a_BlockX, int a_BlockY, int a_BlockZ, eBlockFace a_BlockFace, int a_CursorX, int a_CursorY, int a_CursorZ, eHand a_Hand) +{ + // This function handles three actions: + // (1) Place a block; + // (2) "Use" a block: Interactive with the block, like opening a chest/crafting table/furnace; + // (3) Use the held item targeting a block. E.g. farming. + // + // Sneaking player will not use the block if hand is not empty. + // Frozen player can do nothing. + // In Game Mode Spectator, player cannot use item or place block, but can interactive with some block depending on cBlockInfo::IsUseableBySpectator(BlockType) + // + // If the action failed, we need to send an update of the placed block or inventory to the client. + // + // Actions rejected by plugin will not lead to other attempts. + // E.g., when opening a chest with a dirt in hand, if the plugin rejects opening the chest, the dirt will not be placed. + + // TODO: We are still consuming the items in main hand. Remove this override when the off-hand consumption is handled correctly. + a_Hand = eHand::hMain; + const cItem & HeldItem = (a_Hand == eHand::hOff) ? m_Player->GetInventory().GetShieldSlot() : m_Player->GetEquippedItem(); + cItemHandler * ItemHandler = cItemHandler::GetItemHandler(HeldItem.m_ItemType); + + // TODO: This distance should be calculated from the point that the cursor pointing at, instead of the center of the block // Distance from the block's center to the player's eye height - double dist = (Vector3d(a_BlockX, a_BlockY, a_BlockZ) + Vector3d(0.5, 0.5, 0.5) - m_Player->GetEyePosition()).Length(); - LOGD("HandleRightClick: {%d, %d, %d}, face %d, HeldItem: %s; dist: %.02f", - a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, ItemToFullString(a_HeldItem).c_str(), dist + double Dist = (Vector3d(a_BlockX, a_BlockY, a_BlockZ) + Vector3d(0.5, 0.5, 0.5) - m_Player->GetEyePosition()).Length(); + LOGD("HandleRightClick: {%d, %d, %d}, face %d, Hand: %d, HeldItem: %s; Dist: %.02f", + a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_Hand, ItemToFullString(HeldItem).c_str(), Dist ); // Check the reach distance: // _X 2014-11-25: I've maxed at 5.26 with a Survival client and 5.78 with a Creative client in my tests - double maxDist = m_Player->IsGameModeCreative() ? 5.78 : 5.26; - bool AreRealCoords = (dist <= maxDist); - + double MaxDist = m_Player->IsGameModeCreative() ? 5.78 : 5.26; + bool IsWithinReach = (Dist <= MaxDist); cWorld * World = m_Player->GetWorld(); - - if ( - (a_BlockFace != BLOCK_FACE_NONE) && // The client is interacting with a specific block - IsValidBlock(a_HeldItem.m_ItemType) && - !AreRealCoords - ) - { - AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); - if ((a_BlockX != -1) && (a_BlockY >= 0) && (a_BlockZ != -1)) - { - if (cChunkDef::IsValidHeight(a_BlockY)) - { - World->SendBlockTo(a_BlockX, a_BlockY, a_BlockZ, *m_Player); - } - if (cChunkDef::IsValidHeight(a_BlockY + 1)) - { - World->SendBlockTo(a_BlockX, a_BlockY + 1, a_BlockZ, *m_Player); // 2 block high things - } - if (cChunkDef::IsValidHeight(a_BlockY - 1)) - { - World->SendBlockTo(a_BlockX, a_BlockY - 1, a_BlockZ, *m_Player); // 2 block high things - } - } - m_Player->GetInventory().SendEquippedSlot(); - return; - } - - if (!AreRealCoords) - { - a_BlockFace = BLOCK_FACE_NONE; - } - cPluginManager * PlgMgr = cRoot::Get()->GetPluginManager(); - if (m_Player->IsFrozen() || PlgMgr->CallHookPlayerRightClick(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) - { - // A plugin doesn't agree with the action, replace the block on the client and quit: - if (AreRealCoords) - { - cChunkInterface ChunkInterface(World->GetChunkMap()); - BLOCKTYPE BlockType = World->GetBlock(a_BlockX, a_BlockY, a_BlockZ); - cBlockHandler * BlockHandler = cBlockInfo::GetHandler(BlockType); - BlockHandler->OnCancelRightClick(ChunkInterface, *World, *m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); - - if (a_BlockFace != BLOCK_FACE_NONE) - { - AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); - if (cChunkDef::IsValidHeight(a_BlockY)) - { - World->SendBlockTo(a_BlockX, a_BlockY, a_BlockZ, *m_Player); - } - if (cChunkDef::IsValidHeight(a_BlockY + 1)) - { - World->SendBlockTo(a_BlockX, a_BlockY + 1, a_BlockZ, *m_Player); // 2 block high things - } - if (cChunkDef::IsValidHeight(a_BlockY - 1)) - { - World->SendBlockTo(a_BlockX, a_BlockY - 1, a_BlockZ, *m_Player); // 2 block high things - } - m_Player->GetInventory().SendEquippedSlot(); - } - } - return; - } - - m_NumBlockChangeInteractionsThisTick++; - - if (!CheckBlockInteractionsRate()) - { - Kick("Too many blocks were placed / interacted with per unit time - hacked client?"); - return; - } - - const cItem & Equipped = m_Player->GetInventory().GetEquippedItem(); - - if ((Equipped.m_ItemType != a_HeldItem.m_ItemType) && (a_HeldItem.m_ItemType != -1)) - { - // Only compare ItemType, not meta (torches have different metas) - // The -1 check is there because sometimes the client sends -1 instead of the held item - // Ref.: https://forum.cuberite.org/thread-549-post-4502.html#pid4502 - LOGWARN("Player %s tried to place a block that was not equipped (exp %d, got %d)", - m_Username.c_str(), Equipped.m_ItemType, a_HeldItem.m_ItemType - ); - - // Let's send the current world block to the client, so that it can immediately "let the user know" that they haven't placed the block - if (a_BlockFace != BLOCK_FACE_NONE) - { - AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); - World->SendBlockTo(a_BlockX, a_BlockY, a_BlockZ, *m_Player); - } - return; - } - if (AreRealCoords) + bool Success = false; + if (IsWithinReach && !m_Player->IsFrozen()) { BLOCKTYPE BlockType; NIBBLETYPE BlockMeta; World->GetBlockTypeMeta(a_BlockX, a_BlockY, a_BlockZ, BlockType, BlockMeta); cBlockHandler * BlockHandler = cBlockInfo::GetHandler(BlockType); - if (BlockHandler->IsUseable() && !m_Player->IsCrouched() && (!m_Player->IsGameModeSpectator() || cBlockInfo::IsUseableBySpectator(BlockType))) + bool Placeable = ItemHandler->IsPlaceable() && !m_Player->IsGameModeSpectator(); + bool BlockUsable = BlockHandler->IsUseable() && (!m_Player->IsGameModeSpectator() || cBlockInfo::IsUseableBySpectator(BlockType)); + + if (BlockUsable && !(m_Player->IsCrouched() && !HeldItem.IsEmpty())) { + // use a block + cChunkInterface ChunkInterface(World->GetChunkMap()); if (!PlgMgr->CallHookPlayerUsingBlock(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ, BlockType, BlockMeta)) { - cChunkInterface ChunkInterface(World->GetChunkMap()); if (BlockHandler->OnUse(ChunkInterface, *World, *m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) { // block use was successful, we're done PlgMgr->CallHookPlayerUsedBlock(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ, BlockType, BlockMeta); - return; + Success = true; } } + else + { + // TODO: OnCancelRightClick seems to do the same thing with updating blocks at the end of this function. Need to double check + // A plugin doesn't agree with the action, replace the block on the client and quit: + BlockHandler->OnCancelRightClick(ChunkInterface, *World, *m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); + } } - } - - // Players, who spectate cannot use their items - if (m_Player->IsGameModeSpectator()) - { - return; - } - - short EquippedDamage = Equipped.m_ItemDamage; - cItemHandler * ItemHandler = cItemHandler::GetItemHandler(Equipped.m_ItemType); - - if (ItemHandler->IsPlaceable() && (a_BlockFace != BLOCK_FACE_NONE)) - { - if (!ItemHandler->OnPlayerPlace(*World, *m_Player, Equipped, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) - { - // Placement failed, bail out - return; - } - } - else if ((ItemHandler->IsFood() || ItemHandler->IsDrinkable(EquippedDamage))) - { - if ( - (m_Player->IsSatiated() || m_Player->IsGameModeCreative()) && // Only creative or hungry players can eat - ItemHandler->IsFood() && - (Equipped.m_ItemType != E_ITEM_GOLDEN_APPLE) // Golden apple is a special case, it is used instead of eaten - ) + else if (Placeable) { - // The player is satiated or in creative, and trying to eat - return; + // TODO: Double check that we don't need this for using item and for packet out of range + m_NumBlockChangeInteractionsThisTick++; + if (!CheckBlockInteractionsRate()) + { + Kick("Too many blocks were placed / interacted with per unit time - hacked client?"); + return; + } + if (!PlgMgr->CallHookPlayerRightClick(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) + { + // place a block + Success = ItemHandler->OnPlayerPlace(*World, *m_Player, HeldItem, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ); + } } - m_Player->StartEating(); - if (m_Player->IsFrozen() || PlgMgr->CallHookPlayerEating(*m_Player)) + else { - // A plugin won't let us eat, abort (send the proper packets to the client, too): - m_Player->AbortEating(); + // Use an item in hand with a target block + if (!PlgMgr->CallHookPlayerUsingItem(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) + { + // All plugins agree with using the item + cBlockInServerPluginInterface PluginInterface(*World); + ItemHandler->OnItemUse(World, m_Player, PluginInterface, HeldItem, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); + PlgMgr->CallHookPlayerUsedItem(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ); + Success = true; + } } } - else + if (!Success) { - if (m_Player->IsFrozen() || PlgMgr->CallHookPlayerUsingItem(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ)) + // Update the target block including the block above and below for 2 block high things + AddFaceDirection(a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); + for (int y = a_BlockY - 1; y <= a_BlockY + 1; y++) { - // A plugin doesn't agree with using the item, abort - return; + if (cChunkDef::IsValidHeight(y)) + { + World->SendBlockTo(a_BlockX, y, a_BlockZ, *m_Player); + } } - cBlockInServerPluginInterface PluginInterface(*World); - ItemHandler->OnItemUse(World, m_Player, PluginInterface, Equipped, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace); - PlgMgr->CallHookPlayerUsedItem(*m_Player, a_BlockX, a_BlockY, a_BlockZ, a_BlockFace, a_CursorX, a_CursorY, a_CursorZ); - } - // Charge bow when it's in slot off-hand / shield - if ((a_BlockFace == BLOCK_FACE_NONE) && (m_Player->GetInventory().GetShieldSlot().m_ItemType == E_ITEM_BOW)) - { - m_Player->StartChargingBow(); + // TODO: Send corresponding slot based on hand + m_Player->GetInventory().SendEquippedSlot(); } } @@ -1810,6 +1737,61 @@ void cClientHandle::HandleUseEntity(UInt32 a_TargetEntityID, bool a_IsLeftClick) +void cClientHandle::HandleUseItem(eHand a_Hand) +{ + // Use the held item without targeting a block: eating, drinking, charging a bow, using buckets + // In version 1.8.x, this function shares the same packet id with HandleRightClick. + // In version >= 1.9, there is a new packet id for "Use Item". + + // TODO: We are still consuming the items in main hand. Remove this override when the off-hand consumption is handled correctly. + a_Hand = eHand::hMain; + const cItem & HeldItem = (a_Hand == eHand::hOff) ? m_Player->GetInventory().GetShieldSlot() : m_Player->GetEquippedItem(); + cItemHandler * ItemHandler = cItemHandler::GetItemHandler(HeldItem.m_ItemType); + cWorld * World = m_Player->GetWorld(); + cPluginManager * PlgMgr = cRoot::Get()->GetPluginManager(); + + LOGD("HandleUseItem: Hand: %d; HeldItem: %s", a_Hand, ItemToFullString(HeldItem).c_str()); + + // Use item in main / off hand + // TODO: do we need to sync the current inventory with client if it fails? + if (m_Player->IsFrozen() || m_Player->IsGameModeSpectator()) + { + return; + } + + if (ItemHandler->IsFood() || ItemHandler->IsDrinkable(HeldItem.m_ItemDamage)) + { + if ( + ItemHandler->IsFood() && + (m_Player->IsSatiated() || m_Player->IsGameModeCreative()) && // Only non-creative or hungry players can eat + (HeldItem.m_ItemType != E_ITEM_GOLDEN_APPLE) // Golden apple is a special case, it is used instead of eaten + ) + { + // The player is satiated or in creative, and trying to eat + return; + } + if (!PlgMgr->CallHookPlayerEating(*m_Player)) + { + m_Player->StartEating(); + } + } + else + { + // Use an item in hand without a target block + if (!PlgMgr->CallHookPlayerUsingItem(*m_Player, -1, 255, -1, BLOCK_FACE_NONE, 0, 0, 0)) + { + // All plugins agree with using the item + cBlockInServerPluginInterface PluginInterface(*World); + ItemHandler->OnItemUse(World, m_Player, PluginInterface, HeldItem, -1, 255, -1, BLOCK_FACE_NONE); + PlgMgr->CallHookPlayerUsedItem(*m_Player, -1, 255, -1, BLOCK_FACE_NONE, 0, 0, 0); + } + } +} + + + + + void cClientHandle::HandleRespawn(void) { if (m_Player == nullptr) -- cgit v1.2.3