From 734f79e46917e4cb4e8d579246394f3048b4e1ff Mon Sep 17 00:00:00 2001 From: xRookieFight Date: Mon, 10 Aug 2026 10:19:32 +0300 Subject: [PATCH] fix: don't deadlock when equipping an item from the hand (#2857) --- crates/pumpkin-util/src/lib.rs | 15 ++++++++++++ crates/pumpkin/src/net/java/play.rs | 36 +++++++++++++---------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/pumpkin-util/src/lib.rs b/crates/pumpkin-util/src/lib.rs index bf1c27378..31ebb8684 100644 --- a/crates/pumpkin-util/src/lib.rs +++ b/crates/pumpkin-util/src/lib.rs @@ -271,6 +271,21 @@ impl Hand { pub const fn all() -> [Self; 2] { [Self::Right, Self::Left] } + + /// Converts the `hand` field of a play packet, where `0` is the main hand. + /// + /// This is the opposite of [`TryFrom`], which reads the dominant hand + /// out of the client settings, where `0` is the left hand. + /// + /// # Errors + /// Returns `InvalidHand` if the value is not 0 or 1. + pub const fn from_packet_id(value: i32) -> Result { + match value { + 0 => Ok(Self::Right), + 1 => Ok(Self::Left), + _ => Err(InvalidHand), + } + } } /// Error type for invalid hand conversion. diff --git a/crates/pumpkin/src/net/java/play.rs b/crates/pumpkin/src/net/java/play.rs index 1b35e6430..cca6f808e 100644 --- a/crates/pumpkin/src/net/java/play.rs +++ b/crates/pumpkin/src/net/java/play.rs @@ -2558,17 +2558,13 @@ impl JavaClient { player.update_last_action_time(); let inventory = player.inventory(); - let Ok(hand) = Hand::try_from(use_item.hand.0) else { + let Ok(hand) = Hand::from_packet_id(use_item.hand.0) else { self.kick(TextComponent::text("InvalidHand")).await; return; }; self.update_sequence(player, use_item.sequence.0); - let mut item_in_hand = if hand == Hand::Left { - inventory.held_item().await - } else { - inventory.off_hand_item().await - }; + let mut item_in_hand = inventory.get_stack_in_hand(hand).await; let (item_id, _item) = (item_in_hand.item.id, item_in_hand.item); player @@ -2659,27 +2655,27 @@ impl JavaClient { .await; } } - if let Some(equippable) = held.get_data_component::() { - let mut equipment_guard = inventory.entity_equipment.lock().await; - let current_equipped = equipment_guard.get(equippable.slot); + let equipment_slot = held + .get_data_component::() + .map(|equippable| equippable.slot.clone()); + if let Some(slot) = equipment_slot { + // The equipment lock has to be released before touching the hand again: + // the off hand lives in the same map, so holding it here would deadlock. + let current_equipped = inventory.entity_equipment.lock().await.get(&slot); if current_equipped.are_items_and_components_equal(held) { return; } - player.enqueue_equipment_change(equippable.slot, held).await; + player.enqueue_equipment_change(&slot, held).await; - let equip_item = equipment_guard - .equipment - .entry(equippable.slot.clone()) - .or_insert_with(|| ItemStack::EMPTY.clone()); - if equip_item.is_empty() { - *equip_item = held.clone(); + let equipped = if current_equipped.is_empty() { + let equipped = held.clone(); held.decrement_unless_creative(player.gamemode.load(), 1); + equipped } else { - let old_held = held.clone(); - *held = equip_item.clone(); - *equip_item = old_held; - } + std::mem::replace(held, current_equipped) + }; + inventory.entity_equipment.lock().await.put(&slot, equipped); inventory.set_stack_in_hand(hand, held.clone()).await; } }