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
This commit is contained in:
C0j23T
2025-09-14 15:56:08 +08:00
committed by GitHub
parent 80741dd47a
commit 09f96cc705
7 changed files with 182 additions and 30 deletions

View File

@@ -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()

View File

@@ -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::<EquippableImpl>()
.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::<EquippableImpl>()
.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

View File

@@ -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)?;
}

View File

@@ -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::<EquippableImpl>()
&& 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;
}
}
}

View File

@@ -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::<ConsumableImpl>().is_some() {
// If its food we want to make sure we can actually consume it
if let Some(food) = held.get_data_component::<FoodImpl>() {
@@ -1622,6 +1622,23 @@ impl JavaClient {
.await;
}
}
if let Some(equippable) = held.get_data_component::<EquippableImpl>() {
// 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

View File

@@ -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<bool>) {
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.

View File

@@ -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;