From 09f96cc7058b7ccc3c527a796390db4d66f8766d Mon Sep 17 00:00:00 2001 From: C0j23T <64320358+C0j23T@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:56:08 +0800 Subject: [PATCH] Implement equipping armor and resolve difficulty deadlock (#1170) * feat: implement equipping armour for players * fix: resolve deadlock when updating world difficulty * fix: typo && forgot to clippy * fix: fmt --- .../src/player/player_screen_handler.rs | 8 +++ pumpkin-inventory/src/screen_handler.rs | 37 ++++++++++++- .../src/java/client/play/set_equipment.rs | 2 +- pumpkin/src/entity/player.rs | 39 +++++++++++-- pumpkin/src/net/java/play.rs | 55 ++++++++++++++++++- pumpkin/src/server/mod.rs | 39 +++++++------ pumpkin/src/world/mod.rs | 32 +++++++++++ 7 files changed, 182 insertions(+), 30 deletions(-) diff --git a/pumpkin-inventory/src/player/player_screen_handler.rs b/pumpkin-inventory/src/player/player_screen_handler.rs index 60cc0c248..d8550bb2c 100644 --- a/pumpkin-inventory/src/player/player_screen_handler.rs +++ b/pumpkin-inventory/src/player/player_screen_handler.rs @@ -129,6 +129,10 @@ impl ScreenHandler for PlayerScreenHandler { if !self.insert_item(&mut slot_stack, 9, 45, false).await { return ItemStack::EMPTY.clone(); } + + player + .enqueue_equipment_change(equipment_slot, ItemStack::EMPTY) + .await; } else if equipment_slot.slot_type() == EquipmentType::HumanoidArmor && self .get_slot((8 - equipment_slot.get_entity_slot_id()) as usize) @@ -145,6 +149,10 @@ impl ScreenHandler for PlayerScreenHandler { { return ItemStack::EMPTY.clone(); } + + player + .enqueue_equipment_change(equipment_slot, &stack_prev) + .await; } else if matches!(equipment_slot, EquipmentSlot::OffHand(_)) && slot_index != 45 && self.get_slot(45).await.get_cloned_stack().await.is_empty() diff --git a/pumpkin-inventory/src/screen_handler.rs b/pumpkin-inventory/src/screen_handler.rs index 464ee66d4..eb03c4813 100644 --- a/pumpkin-inventory/src/screen_handler.rs +++ b/pumpkin-inventory/src/screen_handler.rs @@ -6,7 +6,10 @@ use crate::{ }; use async_trait::async_trait; use log::warn; -use pumpkin_data::screen::WindowType; +use pumpkin_data::{ + data_component_impl::{EquipmentSlot, EquipmentType, EquippableImpl}, + screen::WindowType, +}; use pumpkin_protocol::{ codec::item_stack_seralizer::OptionalItemStackHash, java::{ @@ -72,6 +75,7 @@ pub trait InventoryPlayer: Send + Sync { async fn enqueue_property_packet(&self, packet: &CSetContainerProperty); async fn enqueue_slot_set_packet(&self, packet: &CSetPlayerInventory); async fn enqueue_set_held_item_packet(&self, packet: &CSetSelectedSlot); + async fn enqueue_equipment_change(&self, slot: &EquipmentSlot, stack: &ItemStack); } pub async fn offer_or_drop_stack(player: &dyn InventoryPlayer, stack: ItemStack) { @@ -738,6 +742,10 @@ pub trait ScreenHandler: Send + Sync { let slot_stack = slot.get_cloned_stack().await; let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await; + let equipment_slot = cursor_stack + .get_data_component::() + .map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot); + if self .handle_slot_click( player, @@ -753,6 +761,14 @@ pub trait ScreenHandler: Send + Sync { if slot_stack.is_empty() { if !cursor_stack.is_empty() { + if equipment_slot.slot_type() == EquipmentType::HumanoidArmor + && (5..9).contains(&slot_index) + { + player + .enqueue_equipment_change(equipment_slot, &cursor_stack) + .await; + } + let transfer_count = if click_type == MouseClick::Left { cursor_stack.item_count } else { @@ -774,8 +790,27 @@ pub trait ScreenHandler: Send + Sync { // Reverse order of operations, shouldn't affect anything *cursor_stack = taken.clone(); slot.on_take_item(player, &taken).await; + + if (5..9).contains(&slot_index) { + let equipment_slot = cursor_stack + .get_data_component::() + .map_or(&EquipmentSlot::MAIN_HAND, |equippable| { + equippable.slot + }); + player + .enqueue_equipment_change(equipment_slot, ItemStack::EMPTY) + .await; + } } } else if slot.can_insert(&cursor_stack).await { + if equipment_slot.slot_type() == EquipmentType::HumanoidArmor + && (5..9).contains(&slot_index) + { + player + .enqueue_equipment_change(equipment_slot, &cursor_stack) + .await; + } + if ItemStack::are_items_and_components_equal(&slot_stack, &cursor_stack) { let insert_count = if click_type == MouseClick::Left { cursor_stack.item_count diff --git a/pumpkin-protocol/src/java/client/play/set_equipment.rs b/pumpkin-protocol/src/java/client/play/set_equipment.rs index cf0c2de0e..8ab2cc0bb 100644 --- a/pumpkin-protocol/src/java/client/play/set_equipment.rs +++ b/pumpkin-protocol/src/java/client/play/set_equipment.rs @@ -37,7 +37,7 @@ impl ClientPacket for CSetEquipment { let equipment = &self.equipment[i]; let slot = &equipment.0; if i != self.equipment.len() - 1 { - write.write_i8(-128)?; + write.write_i8(*slot | -128)?; } else { write.write_i8(*slot)?; } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 872b2bec5..f0710dd81 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -15,6 +15,7 @@ use pumpkin_protocol::bedrock::client::update_abilities::{ Ability, AbilityLayer, CUpdateAbilities, }; use pumpkin_protocol::bedrock::server::text::SText; +use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_world::chunk::{ChunkData, ChunkEntityData}; use pumpkin_world::inventory::Inventory; use tokio::sync::{Mutex, RwLock}; @@ -23,8 +24,8 @@ use uuid::Uuid; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::damage::DamageType; -use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::data_component_impl::{AttributeModifiersImpl, Operation}; +use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl}; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; use pumpkin_data::particle::Particle; @@ -49,10 +50,10 @@ use pumpkin_protocol::java::client::play::{ CChunkBatchStart, CChunkData, CCloseContainer, CCombatDeath, CDisguisedChatMessage, CEntityAnimation, CEntityPositionSync, CGameEvent, CKeepAlive, COpenScreen, CParticle, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn, - CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetExperience, - CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, CSubtitle, - CSystemChatMessage, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, - MetaDataType, Metadata, PlayerAction, PlayerInfoFlags, PreviousMessage, + CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment, + CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound, + CSubtitle, CSystemChatMessage, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, + GameEvent, MetaDataType, Metadata, PlayerAction, PlayerInfoFlags, PreviousMessage, }; use pumpkin_protocol::java::server::play::SClickSlot; use pumpkin_registry::VanillaDimensionType; @@ -2539,4 +2540,32 @@ impl InventoryPlayer for Player { async fn enqueue_set_held_item_packet(&self, packet: &CSetSelectedSlot) { self.client.enqueue_packet(packet).await; } + + async fn enqueue_equipment_change(&self, slot: &EquipmentSlot, stack: &ItemStack) { + self.world() + .broadcast_packet_except( + &[self.get_entity().entity_uuid], + &CSetEquipment::new( + self.entity_id().into(), + vec![( + slot.discriminant(), + ItemStackSerializer::from(stack.clone()), + )], + ), + ) + .await; + + if let Some(equippable) = stack.get_data_component::() + && let Some(sound) = Sound::from_name( + equippable + .equip_sound + .strip_prefix("minecraft:") + .unwrap_or(equippable.equip_sound), + ) + { + self.world() + .play_sound(sound, SoundCategory::Players, &self.position()) + .await; + } + } } diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 93dd7da37..59f813e7f 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -26,13 +26,13 @@ use crate::server::{Server, seasonal_events}; use crate::world::{World, chunker}; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::block_properties::{BlockProperties, WaterLikeProperties}; -use pumpkin_data::data_component_impl::{ConsumableImpl, EquipmentSlot, FoodImpl}; +use pumpkin_data::data_component_impl::{ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl}; use pumpkin_data::item::Item; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::{Block, BlockDirection, BlockState}; use pumpkin_inventory::InventoryError; use pumpkin_inventory::player::player_inventory::PlayerInventory; -use pumpkin_inventory::screen_handler::ScreenHandler; +use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler}; use pumpkin_macros::send_cancellable; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::{ @@ -1602,7 +1602,7 @@ impl JavaClient { None, ) }; - let held = item_in_hand.lock().await; + let mut held = item_in_hand.lock().await; if held.get_data_component::().is_some() { // If its food we want to make sure we can actually consume it if let Some(food) = held.get_data_component::() { @@ -1622,6 +1622,23 @@ impl JavaClient { .await; } } + if let Some(equippable) = held.get_data_component::() { + // If it can be equipped we want to makr sure we can actually equip it + player + .enqueue_equipment_change(equippable.slot, &held) + .await; + + let binding = inventory.entity_equipment.lock().await.get(equippable.slot); + let mut equip_item = binding.lock().await; + if equip_item.is_empty() { + *equip_item = held.clone(); + held.decrement_unless_creative(player.gamemode.load(), 1); + } else { + let binding = held.clone(); + *held = equip_item.clone(); + *equip_item = binding; + } + } send_cancellable! {{ event; @@ -1662,6 +1679,38 @@ impl JavaClient { if valid_slot && is_legal { let mut player_screen_handler = player.player_screen_handler.lock().await; + + let is_armor_equipped = player_screen_handler + .get_slot(packet.slot as usize) + .await + .get_stack() + .await + .lock() + .await + .are_equal(&item_stack); + if !is_armor_equipped { + if (5..9).contains(&packet.slot) { + player + .enqueue_equipment_change( + &match packet.slot { + 5 => EquipmentSlot::HEAD, + 6 => EquipmentSlot::CHEST, + 7 => EquipmentSlot::LEGS, + 8 => EquipmentSlot::FEET, + _ => unreachable!(), + }, + &item_stack, + ) + .await; + } else if (36..45).contains(&packet.slot) { + let slot = packet.slot - 36; + if player.inventory().get_selected_slot() == slot as u8 { + let equipment = &[(EquipmentSlot::MAIN_HAND, item_stack.clone())]; + player.living_entity.send_equipment_changes(equipment).await; + } + } + } + player_screen_handler .get_slot(packet.slot as usize) .await diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index cbfbccf00..034fa705b 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -445,27 +445,26 @@ impl Server { /// This function does not handle the actual mob spawn options update, which is a TODO item for future implementation. pub async fn set_difficulty(&self, difficulty: Difficulty, force_update: Option) { let mut level_info = self.level_info.write().await; - if force_update.unwrap_or_default() || !level_info.difficulty_locked { - level_info.difficulty = if BASIC_CONFIG.hardcore { - Difficulty::Hard - } else { - difficulty - }; - // Minecraft server updates mob spawn options here - // but its not implemented in Pumpkin yet - // todo: update mob spawn options - - for world in &*self.worlds.read().await { - world.level_info.write().await.difficulty = level_info.difficulty; - } - - self.broadcast_packet_all(&CChangeDifficulty::new( - level_info.difficulty as u8, - level_info.difficulty_locked, - )) - .await; - drop(level_info); + if level_info.difficulty_locked && !force_update.unwrap_or_default() { + return; } + + let difficulty = if BASIC_CONFIG.hardcore { + Difficulty::Hard + } else { + difficulty + }; + + level_info.difficulty = difficulty; + let locked = level_info.difficulty_locked; + drop(level_info); + + for world in &*self.worlds.read().await { + world.level_info.write().await.difficulty = difficulty; + } + + self.broadcast_packet_all(&CChangeDifficulty::new(difficulty as u8, locked)) + .await; } /// Searches for a player by their username across all worlds. diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index f33e05afe..555208a41 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1600,6 +1600,38 @@ impl World { buf.into(), )) .await; + }; + + { + let mut equipment_list = Vec::new(); + + equipment_list.push(( + EquipmentSlot::MAIN_HAND.discriminant(), + existing_player.inventory.held_item().lock().await.clone(), + )); + + for (slot, item_arc_mutex) in &existing_player + .inventory + .entity_equipment + .lock() + .await + .equipment + { + let item_stack = item_arc_mutex.lock().await.clone(); + equipment_list.push((slot.discriminant(), item_stack)); + } + + let equipment: Vec<(i8, ItemStackSerializer)> = equipment_list + .iter() + .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) + .collect(); + + client + .enqueue_packet(&CSetEquipment::new( + existing_player.entity_id().into(), + equipment, + )) + .await; } } player.send_client_information().await;