From 5f60d238043c2a805f5b277314c194adeaf97a5d Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 21 Jun 2026 21:54:10 +0200 Subject: [PATCH] feat: villager Professions --- pumpkin-codegen/src/game_rules.rs | 10 + pumpkin-data/src/data_component_impl.rs | 197 +++++- .../src/merchant/merchant_screen_handler.rs | 21 +- pumpkin-inventory/src/screen_handler.rs | 51 ++ .../src/java/client/play/merchant_offers.rs | 4 +- pumpkin/src/block/blocks/lectern.rs | 41 +- pumpkin/src/block/entities/lectern.rs | 9 +- pumpkin/src/entity/mob/mod.rs | 16 + pumpkin/src/entity/mod.rs | 8 + pumpkin/src/entity/passive/villager/data.rs | 37 +- pumpkin/src/entity/passive/villager/mod.rs | 648 ++++++++++++++++-- pumpkin/src/entity/player.rs | 4 + pumpkin/src/entity/vehicle/minecart.rs | 12 +- pumpkin/src/net/java/play.rs | 6 +- 14 files changed, 978 insertions(+), 86 deletions(-) diff --git a/pumpkin-codegen/src/game_rules.rs b/pumpkin-codegen/src/game_rules.rs index 15e55ae1e..8bc1fba6c 100644 --- a/pumpkin-codegen/src/game_rules.rs +++ b/pumpkin-codegen/src/game_rules.rs @@ -28,6 +28,16 @@ pub fn build() -> TokenStream { let i = n.as_i64().unwrap(); (quote! { Int }, quote! { i64 }, quote! { #i }) } + Value::Object(obj) => { + let default_val = obj.get("default").expect("Object game rule missing default value"); + match default_val { + Value::Number(n) if n.is_i64() => { + let i = n.as_i64().unwrap(); + (quote! { Int }, quote! { i64 }, quote! { #i }) + } + _ => panic!("Unsupported default value type in object for key '{raw_name}'"), + } + } _ => panic!("Unsupported value type for key '{raw_name}'"), }; diff --git a/pumpkin-data/src/data_component_impl.rs b/pumpkin-data/src/data_component_impl.rs index 9db5e9c6f..5516f399d 100644 --- a/pumpkin-data/src/data_component_impl.rs +++ b/pumpkin-data/src/data_component_impl.rs @@ -3,10 +3,11 @@ use crate::attributes::Attributes; use crate::data_component::DataComponent; use crate::data_component::DataComponent::{ - AttributeModifiers, BlocksAttacks, ChargedProjectiles, Consumable, CustomData, CustomName, - Damage, DamageResistant, DeathProtection, Enchantable, Enchantments, Equippable, - FireworkExplosion, Fireworks, Food, ItemModel, ItemName, JukeboxPlayable, MapId, MaxDamage, - MaxStackSize, PotionContents, StoredEnchantments, Tool, Unbreakable, UseCooldown, Weapon, + AttributeModifiers, BlockEntityData, BlocksAttacks, BundleContents, ChargedProjectiles, + Consumable, Container, CustomData, CustomName, Damage, DamageResistant, DeathProtection, + Enchantable, Enchantments, Equippable, FireworkExplosion, Fireworks, Food, ItemModel, ItemName, + JukeboxPlayable, MapId, MaxDamage, MaxStackSize, PotionContents, StoredEnchantments, Tool, + Unbreakable, UseCooldown, Weapon, }; use crate::effect::{self, StatusEffect}; use crate::entity_type::EntityType; @@ -70,6 +71,15 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option { Some(ChargedProjectilesImpl::read_data(data)?.to_dyn()) } + DataComponent::BlockEntityData => Some(BlockEntityDataImpl::read_data(data)?.to_dyn()), + DataComponent::BundleContents => Some(BundleContentsImpl::read_data(data)?.to_dyn()), + DataComponent::Container => Some(ContainerImpl::read_data(data)?.to_dyn()), + DataComponent::WrittenBookContent => { + Some(WrittenBookContentImpl::read_data(data)?.to_dyn()) + } + DataComponent::WritableBookContent => { + Some(WritableBookContentImpl::read_data(data)?.to_dyn()) + } _ => None, } } @@ -1559,8 +1569,79 @@ impl DataComponentImpl for ChargedProjectilesImpl { default_impl!(ChargedProjectiles); } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct BundleContentsImpl; +#[derive(Clone)] +pub struct BundleContentsImpl { + pub items: Vec, +} +impl PartialEq for BundleContentsImpl { + fn eq(&self, _other: &Self) -> bool { + false + } +} +impl Eq for BundleContentsImpl {} +impl std::fmt::Debug for BundleContentsImpl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BundleContentsImpl") + } +} +impl BundleContentsImpl { + pub fn read_data(tag: &NbtTag) -> Option { + let mut items = Vec::new(); + if let NbtTag::List(l) = tag { + for item_tag in l { + if let NbtTag::Compound(c) = item_tag + && let Some(stack) = crate::item_stack::ItemStack::read_item_stack(c) + { + items.push(stack); + } + } + } + Some(Self { items }) + } + + pub fn get_weight(&self) -> u32 { + self.items + .iter() + .map(|item| item.item_count as u32 * (64 / item.get_max_stack_size() as u32).max(1)) + .sum() + } + + pub fn try_insert(&mut self, stack: &mut crate::item_stack::ItemStack) -> bool { + if stack.is_empty() || stack.get_data_component::().is_some() { + return false; // Can't put bundles in bundles + } + let weight_per_item = (64 / stack.get_max_stack_size() as u32).max(1); + let mut inserted_anything = false; + + while stack.item_count > 0 && self.get_weight() + weight_per_item <= 64 { + if let Some(top) = self.items.first_mut() + && crate::item_stack::ItemStack::are_items_and_components_equal(top, stack) + && top.item_count < top.get_max_stack_size() + { + top.item_count += 1; + stack.item_count -= 1; + inserted_anything = true; + continue; + } + self.items.insert(0, stack.copy_with_count(1)); + stack.item_count -= 1; + inserted_anything = true; + } + + inserted_anything + } + + pub fn try_extract(&mut self) -> Option { + if self.items.is_empty() { + None + } else { + Some(self.items.remove(0)) + } + } +} +impl DataComponentImpl for BundleContentsImpl { + default_impl!(BundleContents); +} /// Status effect instance for potion contents #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct StatusEffectInstance { @@ -1898,9 +1979,53 @@ pub struct PotionDurationScaleImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct SuspiciousStewEffectsImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct WritableBookContentImpl; +pub struct WritableBookContentImpl { + pub pages: Vec, +} + +impl WritableBookContentImpl { + pub fn read_data(tag: &NbtTag) -> Option { + let mut pages = Vec::new(); + if let NbtTag::Compound(c) = tag + && let Some(NbtTag::List(l)) = c.get("pages") + { + for _ in l { + pages.push(String::new()); + } + } + Some(Self { pages }) + } +} + +use crate::data_component::DataComponent::{WritableBookContent, WrittenBookContent}; + +impl DataComponentImpl for WritableBookContentImpl { + default_impl!(WritableBookContent); +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct WrittenBookContentImpl; +pub struct WrittenBookContentImpl { + pub pages: Vec, +} + +impl WrittenBookContentImpl { + pub fn read_data(tag: &NbtTag) -> Option { + let mut pages = Vec::new(); + if let NbtTag::Compound(c) = tag + && let Some(NbtTag::List(l)) = c.get("pages") + { + for _ in l { + pages.push(String::new()); + } + } + Some(Self { pages }) + } +} + +impl DataComponentImpl for WrittenBookContentImpl { + default_impl!(WrittenBookContent); +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct TrimImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -1909,8 +2034,22 @@ pub struct DebugStickStateImpl; pub struct EntityDataImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BucketEntityDataImpl; -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct BlockEntityDataImpl; +#[derive(Clone, Debug, PartialEq)] +pub struct BlockEntityDataImpl { + pub nbt: pumpkin_nbt::compound::NbtCompound, +} +impl BlockEntityDataImpl { + pub fn read_data(tag: &NbtTag) -> Option { + if let NbtTag::Compound(c) = tag { + Some(Self { nbt: c.clone() }) + } else { + None + } + } +} +impl DataComponentImpl for BlockEntityDataImpl { + default_impl!(BlockEntityData); +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct InstrumentImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -2129,8 +2268,42 @@ pub struct BannerPatternsImpl; pub struct BaseColorImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct PotDecorationsImpl; -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct ContainerImpl; +#[derive(Clone)] +pub struct ContainerImpl { + pub items: Vec<(u8, crate::item_stack::ItemStack)>, +} +impl PartialEq for ContainerImpl { + fn eq(&self, _other: &Self) -> bool { + false + } +} +impl Eq for ContainerImpl {} +impl std::fmt::Debug for ContainerImpl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ContainerImpl") + } +} +impl ContainerImpl { + pub fn read_data(tag: &NbtTag) -> Option { + let mut items = Vec::new(); + if let NbtTag::List(l) = tag { + for item_tag in l { + if let NbtTag::Compound(c) = item_tag + && let Some(slot) = c.get_int("slot") + && let Some(item_compound) = c.get_compound("item") + && let Some(stack) = + crate::item_stack::ItemStack::read_item_stack(item_compound) + { + items.push((slot as u8, stack)); + } + } + } + Some(Self { items }) + } +} +impl DataComponentImpl for ContainerImpl { + default_impl!(Container); +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BlockStateImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] diff --git a/pumpkin-inventory/src/merchant/merchant_screen_handler.rs b/pumpkin-inventory/src/merchant/merchant_screen_handler.rs index 1c0b39efa..19e1b427b 100644 --- a/pumpkin-inventory/src/merchant/merchant_screen_handler.rs +++ b/pumpkin-inventory/src/merchant/merchant_screen_handler.rs @@ -18,6 +18,7 @@ pub struct MerchantScreenHandler { behaviour: ScreenHandlerBehaviour, selected_offer: usize, pub offers: Vec, + pub on_trade: Option>, } impl MerchantScreenHandler { @@ -32,6 +33,7 @@ impl MerchantScreenHandler { behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Merchant)), selected_offer: 0, offers, + on_trade: None, }; inventory.on_open().await; @@ -183,21 +185,26 @@ impl ScreenHandler for MerchantScreenHandler { let result_stack = result_slot.get_cloned_stack().await; if !result_stack.is_empty() { // Consume inputs - let offer = &self.offers[self.selected_offer]; + let (count_a, count_b, offer_xp) = { + let offer = &mut self.offers[self.selected_offer]; + offer.uses += 1; + let count_b = offer.cost_b.as_ref().map(|c| c.0.item_count); + (offer.base_cost_a.0.item_count, count_b, offer.xp) + }; let input_a = self.inventory.get_stack(0).await; let mut input_a = input_a.lock().await; - input_a.decrement(offer.base_cost_a.0.item_count); + input_a.decrement(count_a); if input_a.is_empty() { *input_a = ItemStack::EMPTY.clone(); } drop(input_a); self.get_behaviour().slots[0].mark_dirty().await; - if let Some(cost_b) = &offer.cost_b { + if let Some(count_b) = count_b { let input_b = self.inventory.get_stack(1).await; let mut input_b = input_b.lock().await; - input_b.decrement(cost_b.0.item_count); + input_b.decrement(count_b); if input_b.is_empty() { *input_b = ItemStack::EMPTY.clone(); } @@ -206,7 +213,11 @@ impl ScreenHandler for MerchantScreenHandler { } // Award XP - player.award_experience(offer.xp).await; + player.award_experience(offer_xp).await; + + if let Some(on_trade) = &self.on_trade { + on_trade(self.selected_offer); + } } } } diff --git a/pumpkin-inventory/src/screen_handler.rs b/pumpkin-inventory/src/screen_handler.rs index 896516816..4e8b1a165 100644 --- a/pumpkin-inventory/src/screen_handler.rs +++ b/pumpkin-inventory/src/screen_handler.rs @@ -121,6 +121,7 @@ pub type PlayerFuture<'a, T> = Pin + Send + 'a>>; /// /// Implementors are typically player entities that can open containers. pub trait InventoryPlayer: Send + Sync { + fn as_any(&self) -> &dyn std::any::Any; /// Drops an item into the world. /// /// # Arguments @@ -1059,6 +1060,56 @@ pub trait ScreenHandler: Send + Sync { let slot_stack = slot.get_cloned_stack().await; let mut cursor_stack = self.get_behaviour().cursor_stack.lock().await; + if click_type == MouseClick::Right { + let mut intercepted = false; + + if !cursor_stack.is_empty() { + let stack_guard = slot.get_stack().await; + let mut inner_slot_stack = stack_guard.lock().await; + if let Some(bundle) = inner_slot_stack.get_data_component_mut::() + && bundle.try_insert(&mut cursor_stack) { + intercepted = true; + } + } + + if !intercepted && !slot_stack.is_empty() + && let Some(bundle) = cursor_stack.get_data_component_mut::() { + let stack_guard = slot.get_stack().await; + let mut inner_slot_stack = stack_guard.lock().await; + if bundle.try_insert(&mut inner_slot_stack) { + if inner_slot_stack.item_count == 0 { + *inner_slot_stack = ItemStack::EMPTY.clone(); + } + intercepted = true; + } + } + + if !intercepted && cursor_stack.is_empty() { + let stack_guard = slot.get_stack().await; + let mut inner_slot_stack = stack_guard.lock().await; + if let Some(bundle) = inner_slot_stack.get_data_component_mut::() + && let Some(extracted) = bundle.try_extract() { + *cursor_stack = extracted; + intercepted = true; + } + } + + if !intercepted && slot_stack.is_empty() + && let Some(bundle) = cursor_stack.get_data_component_mut::() + && let Some(extracted) = bundle.try_extract() { + slot.set_stack(extracted).await; + intercepted = true; + } + + if intercepted { + if cursor_stack.item_count == 0 { + *cursor_stack = ItemStack::EMPTY.clone(); + } + slot.mark_dirty().await; + return; + } + } + let equipment_slot = cursor_stack .get_data_component::() .map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot); diff --git a/pumpkin-protocol/src/java/client/play/merchant_offers.rs b/pumpkin-protocol/src/java/client/play/merchant_offers.rs index 6447afe13..632cc45e7 100644 --- a/pumpkin-protocol/src/java/client/play/merchant_offers.rs +++ b/pumpkin-protocol/src/java/client/play/merchant_offers.rs @@ -7,9 +7,9 @@ use crate::codec::item_stack_seralizer::ItemStackSerializer; #[derive(Serialize, Deserialize, Clone)] pub struct MerchantOffer { - pub base_cost_a: ItemStackSerializer<'static>, + pub base_cost_a: ItemStackSerializer<'static>, // TODO: item cost pub output: ItemStackSerializer<'static>, - pub cost_b: Option>, + pub cost_b: Option>, // TODO: item cost pub is_disabled: bool, pub uses: i32, pub max_uses: i32, diff --git a/pumpkin/src/block/blocks/lectern.rs b/pumpkin/src/block/blocks/lectern.rs index 7ee5f9537..4bdf857c6 100644 --- a/pumpkin/src/block/blocks/lectern.rs +++ b/pumpkin/src/block/blocks/lectern.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use crate::block::entities::lectern::LecternBlockEntity; use crate::block::registry::BlockActionResult; use crate::block::{ - BlockBehaviour, BlockFuture, BrokenArgs, NormalUseArgs, OnPlaceArgs, PlacedArgs, - UseWithItemArgs, + BlockBehaviour, BlockFuture, BrokenArgs, GetComparatorOutputArgs, NormalUseArgs, OnPlaceArgs, + PlacedArgs, UseWithItemArgs, }; use crate::entity::Entity; use crate::entity::item::ItemEntity; @@ -148,4 +148,41 @@ impl BlockBehaviour for LecternBlock { } }) } + + fn get_comparator_output<'a>( + &'a self, + args: GetComparatorOutputArgs<'a>, + ) -> BlockFuture<'a, Option> { + Box::pin(async move { + if let Some(block_entity) = args.world.get_block_entity(args.position) + && let Some(lectern_entity) = + block_entity.as_any().downcast_ref::() + { + let book_guard = lectern_entity.book.lock().await; + if book_guard.is_empty() { + return Some(0); + } + + let page = lectern_entity + .page + .load(std::sync::atomic::Ordering::Relaxed) as f32; + let mut pages = 1.0; + if let Some(comp) = book_guard.get_data_component::() { + pages = comp.pages.len().max(1) as f32; + } else if let Some(comp) = book_guard.get_data_component::() { + pages = comp.pages.len().max(1) as f32; + } + + let output: f32 = if pages > 1.0 { + 1.0 + 14.0 * page / (pages - 1.0) + } else { + 15.0 + }; + + Some(output.floor() as u8) + } else { + Some(0) + } + }) + } } diff --git a/pumpkin/src/block/entities/lectern.rs b/pumpkin/src/block/entities/lectern.rs index 3e3a822d2..727d26a0f 100644 --- a/pumpkin/src/block/entities/lectern.rs +++ b/pumpkin/src/block/entities/lectern.rs @@ -3,10 +3,11 @@ use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; use std::{ any::Any, + future::Future, pin::Pin, sync::{ Arc, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, }; use tokio::sync::Mutex; @@ -17,6 +18,7 @@ use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture}; pub struct LecternBlockEntity { pub position: BlockPos, pub book: Arc>, + pub page: AtomicUsize, pub dirty: AtomicBool, } @@ -41,9 +43,12 @@ impl BlockEntity for LecternBlockEntity { |stack| Arc::new(Mutex::new(stack)), ); + let page = nbt.get_int("Page").unwrap_or(0).max(0) as usize; + Self { position, book, + page: AtomicUsize::new(page), dirty: AtomicBool::new(false), } } @@ -59,6 +64,7 @@ impl BlockEntity for LecternBlockEntity { book.write_item_stack(&mut book_nbt); nbt.put_compound("Book", book_nbt); } + nbt.put_int("Page", self.page.load(Ordering::Relaxed) as i32); }) } @@ -87,6 +93,7 @@ impl LecternBlockEntity { Self { position, book: Arc::new(Mutex::new(ItemStack::EMPTY.clone())), + page: AtomicUsize::new(0), dirty: AtomicBool::new(false), } } diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 8327032b2..fc072ccd1 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -397,6 +397,14 @@ pub trait Mob: EntityBase + Send + Sync { fn get_mob_entity(&self) -> &MobEntity; + fn get_job_site(&self) -> Option { + None + } + + fn get_home(&self) -> Option { + None + } + fn get_path_aware_entity(&self) -> Option<&dyn PathAwareEntity> { None } @@ -675,6 +683,14 @@ impl EntityBase for T { .is_some_and(PathAwareEntity::is_panicking) } + fn get_job_site_pos(&self) -> Option { + ::get_job_site(self) + } + + fn get_home_pos(&self) -> Option { + ::get_home(self) + } + fn as_nbt_storage(&self) -> &dyn NBTStorage { self } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 8644c0bb0..021ca3aba 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -147,6 +147,14 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { }) } + fn get_job_site_pos(&self) -> Option { + None + } + + fn get_home_pos(&self) -> Option { + None + } + fn as_any(&self) -> &dyn std::any::Any where Self: Sized, diff --git a/pumpkin/src/entity/passive/villager/data.rs b/pumpkin/src/entity/passive/villager/data.rs index 957f6bc17..70270a563 100644 --- a/pumpkin/src/entity/passive/villager/data.rs +++ b/pumpkin/src/entity/passive/villager/data.rs @@ -26,23 +26,30 @@ pub enum GossipType { Trading = 4, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] pub struct VillagerData { - pub r#type: VillagerType, - pub profession: VillagerProfession, - pub level: i32, + pub r#type: VarInt, + pub profession: VarInt, + pub level: VarInt, } -impl Serialize for VillagerData { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut state = serializer.serialize_struct("VillagerData", 3)?; - state.serialize_field("type", &VarInt(self.r#type as i32))?; - state.serialize_field("profession", &VarInt(self.profession as i32))?; - state.serialize_field("level", &VarInt(self.level))?; - state.end() +impl VillagerData { + #[must_use] + pub const fn new(r#type: VillagerType, profession: VillagerProfession, level: i32) -> Self { + Self { + r#type: VarInt(r#type as i32), + profession: VarInt(profession as i32), + level: VarInt(level), + } + } + + #[must_use] + pub fn type_enum(&self) -> VillagerType { + VillagerType::from_i32(self.r#type.0).unwrap_or(VillagerType::Plains) + } + + #[must_use] + pub fn profession_enum(&self) -> VillagerProfession { + VillagerProfession::from_i32(self.profession.0).unwrap_or(VillagerProfession::None) } } diff --git a/pumpkin/src/entity/passive/villager/mod.rs b/pumpkin/src/entity/passive/villager/mod.rs index dd8e55711..9a07ec22a 100644 --- a/pumpkin/src/entity/passive/villager/mod.rs +++ b/pumpkin/src/entity/passive/villager/mod.rs @@ -3,16 +3,24 @@ use std::sync::atomic::{AtomicI32, AtomicI64, Ordering}; use std::sync::{Arc, Weak}; use uuid::Uuid; -use pumpkin_data::entity::EntityType; +use crate::block::blocks::bed::BedBlock; +use pumpkin_data::Block; +use pumpkin_data::block_properties::{ + BedPart, BlockProperties, WhiteBedLikeProperties as BedProperties, +}; +use pumpkin_data::entity::{EntityPose, EntityType}; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tag::Taggable; use pumpkin_data::tracked_data::TrackedData; use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler; use pumpkin_inventory::screen_handler::{ BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, }; use pumpkin_nbt::compound::NbtCompound; -use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::{CMerchantOffers, Metadata}; +use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use pumpkin_util::text::TextComponent; use pumpkin_world::inventory::SimpleInventory; use tokio::sync::Mutex; @@ -44,16 +52,15 @@ pub struct VillagerEntity { pub inventory: Arc>>>>, pub merchant_inventory: Arc, pub offers: Mutex>, + pub job_site: std::sync::Mutex>, + pub home_pos: std::sync::Mutex>, + pub self_weak: std::sync::Mutex>>, } impl VillagerEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let villager_data = VillagerData { - r#type: VillagerType::Plains, - profession: VillagerProfession::None, - level: 1, - }; + let villager_data = VillagerData::new(VillagerType::Plains, VillagerProfession::None, 1); let inventory = Arc::new(Mutex::new( (0..8) .map(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))) @@ -71,8 +78,12 @@ impl VillagerEntity { inventory, merchant_inventory: Arc::new(SimpleInventory::new(3)), offers: Mutex::new(Vec::new()), + job_site: std::sync::Mutex::new(None), + home_pos: std::sync::Mutex::new(None), + self_weak: std::sync::Mutex::new(None), }; let mob_arc = Arc::new(villager); + *mob_arc.self_weak.lock().unwrap() = Some(Arc::downgrade(&mob_arc)); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); Arc::downgrade(&mob_arc) @@ -193,9 +204,12 @@ impl VillagerEntity { } pub async fn set_villager_data(&self, data: VillagerData) { - let mut villager_data = self.villager_data.lock().await; - let old_profession = villager_data.profession; - *villager_data = data; + let old_profession = { + let mut villager_data = self.villager_data.lock().await; + let old_profession = villager_data.profession; + *villager_data = data; + old_profession + }; self.get_entity().send_meta_data(&[Metadata::new( TrackedData::VILLAGER_DATA, MetaDataType::VILLAGER_DATA, @@ -203,20 +217,20 @@ impl VillagerEntity { )]); if old_profession != data.profession { - self.generate_trades(data.profession, data.level).await; - if let Some(sound) = data.profession.work_sound() { + self.generate_trades(data.profession_enum(), data.level.0) + .await; + if let Some(sound) = data.profession_enum().work_sound() { self.get_entity().play_sound(sound); } } } - pub async fn generate_trades(&self, profession: VillagerProfession, level: i32) { + pub async fn add_trades(&self, profession: VillagerProfession, level: i32) { use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use rand::seq::IndexedRandom; use std::borrow::Cow; let mut offers = self.offers.lock().await; - offers.clear(); if let Some(trade_set) = profession.trade_set(level) { let mut rng = rand::rng(); @@ -247,6 +261,11 @@ impl VillagerEntity { } } + pub async fn generate_trades(&self, profession: VillagerProfession, level: i32) { + self.offers.lock().await.clear(); + self.add_trades(profession, level).await; + } + pub fn set_unhappy(&self) { let entity = self.get_entity(); entity @@ -256,23 +275,27 @@ impl VillagerEntity { entity.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo); } - pub const fn open_trading_screen(&self, _player: &Arc) { - // let self_weak = self.self_arc.lock().await; - // if let Some(self_arc) = self_weak.as_ref().and_then(std::sync::Weak::upgrade) { - // player.open_handled_screen(&*self_arc, None); + pub async fn open_trading_screen(&self, player: &Arc) { + use pumpkin_protocol::codec::var_int::VarInt; + use pumpkin_protocol::java::client::play::CMerchantOffers; - // let offers = self.offers.lock().await; - // let villager_data = self.villager_data.lock().await; + // Open the merchant screen and then send the current offers packet + if let Some(sync_id) = player.open_handled_screen(self, None).await { + let offers = self.offers.lock().await.clone(); + let villager_data = self.villager_data.lock().await; - // player.client.enqueue_packet(&CMerchantOffers::new( - // player.screen_handler_sync_id.load(Ordering::Relaxed).into(), - // offers.clone(), - // VarInt(villager_data.level), - // VarInt(self.xp.load(Ordering::Relaxed)), - // true, - // true, - // )); - // } + player + .client + .enqueue_packet(&CMerchantOffers::new( + VarInt(sync_id as i32), + offers, + villager_data.level, + VarInt(self.xp.load(Ordering::Relaxed)), + true, + true, + )) + .await; + } } } @@ -281,17 +304,92 @@ impl ScreenHandlerFactory for VillagerEntity { &'a self, sync_id: u8, player_inventory: &'a Arc, - _player: &'a dyn InventoryPlayer, + player: &'a dyn InventoryPlayer, ) -> BoxFuture<'a, Option> { Box::pin(async move { let offers = self.offers.lock().await; - let handler = MerchantScreenHandler::new( + let self_weak = self.self_weak.lock().unwrap().clone().unwrap(); + let player_uuid = player + .as_any() + .downcast_ref::() + .map_or_else(uuid::Uuid::nil, |p| p.get_entity().entity_uuid); + let world = self.get_entity().world.load().clone(); + + let mut handler = MerchantScreenHandler::new( sync_id, player_inventory, self.merchant_inventory.clone(), offers.clone(), ) .await; + + handler.on_trade = Some(Box::new(move |offer_index| { + if let Some(villager) = self_weak.upgrade() { + let world = world.clone(); + tokio::spawn(async move { + if let Some(player) = world.get_player_by_uuid(player_uuid) { + let mut offers = villager.offers.lock().await; + if offer_index < offers.len() { + let offer = &mut offers[offer_index]; + offer.uses += 1; + + let xp_gain = offer.xp; + let current_xp = + villager.xp.fetch_add(xp_gain, Ordering::Relaxed) + xp_gain; + + let mut data = villager.villager_data.lock().await; + let current_level = data.level.0; + if current_level < 5 { + let max_xp = match current_level { + 1 => 10, + 2 => 70, + 3 => 150, + 4 => 250, + _ => 0, + }; + if current_xp >= max_xp { + data.level.0 += 1; + let new_level = data.level.0; + let prof = data.profession_enum(); + drop(data); + + // Level up! Add new trades for the new level + villager.add_trades(prof, new_level).await; + + // Play sound & particles for level up! + let entity = villager.get_entity(); + entity.world.load().send_entity_status( + entity, + pumpkin_data::entity::EntityStatus::VillagerHappy, + ); + entity.play_sound( + pumpkin_data::sound::Sound::EntityVillagerCelebrate, + ); + } else { + drop(data); + } + } else { + drop(data); + } + + let current_level = villager.villager_data.lock().await.level; + player + .client + .enqueue_packet(&CMerchantOffers::new( + VarInt(sync_id as i32), + offers.clone(), + current_level, + VarInt(current_xp), + true, + true, + )) + .await; + } + } + }); + } + })); + Some(Arc::new(Mutex::new(handler)) as SharedScreenHandler) }) } @@ -308,9 +406,9 @@ impl NBTStorage for VillagerEntity { self.mob_entity.living_entity.entity.write_nbt(nbt).await; let data = self.villager_data.lock().await; let mut villager_data_nbt = NbtCompound::new(); - villager_data_nbt.put_int("Type", data.r#type as i32); - villager_data_nbt.put_int("Profession", data.profession as i32); - villager_data_nbt.put_int("Level", data.level); + villager_data_nbt.put_int("Type", data.r#type.0); + villager_data_nbt.put_int("Profession", data.profession.0); + villager_data_nbt.put_int("Level", data.level.0); nbt.put_compound("VillagerData", villager_data_nbt); nbt.put_int("FoodLevel", self.food_level.load(Ordering::Relaxed)); @@ -321,6 +419,58 @@ impl NBTStorage for VillagerEntity { ); nbt.put_int("RestocksToday", self.restocks_today.load(Ordering::Relaxed)); + let job_site_pos = *self.job_site.lock().unwrap(); + if let Some(pos) = job_site_pos { + nbt.put_int("JobSiteX", pos.0.x); + nbt.put_int("JobSiteY", pos.0.y); + nbt.put_int("JobSiteZ", pos.0.z); + } + + let home_pos = *self.home_pos.lock().unwrap(); + if let Some(pos) = home_pos { + nbt.put_int("HomeX", pos.0.x); + nbt.put_int("HomeY", pos.0.y); + nbt.put_int("HomeZ", pos.0.z); + } + + // Save Offers + { + let offers = self.offers.lock().await; + let mut recipes = Vec::new(); + for offer in offers.iter() { + let mut recipe = NbtCompound::new(); + + let mut buy = NbtCompound::new(); + offer.base_cost_a.0.write_item_stack(&mut buy); + recipe.put_compound("buy", buy); + + if let Some(cost_b) = &offer.cost_b + && !cost_b.0.is_empty() + { + let mut buy_b = NbtCompound::new(); + cost_b.0.write_item_stack(&mut buy_b); + recipe.put_compound("buyB", buy_b); + } + + let mut sell_item = NbtCompound::new(); + offer.output.0.write_item_stack(&mut sell_item); + recipe.put_compound("sell", sell_item); + + recipe.put_int("uses", offer.uses); + recipe.put_int("maxUses", offer.max_uses); + recipe.put_bool("rewardExp", !offer.is_disabled); + recipe.put_int("xp", offer.xp); + recipe.put_float("priceMultiplier", offer.price_multiplier); + recipe.put_int("specialPrice", offer.special_price); + recipe.put_int("demand", offer.demand); + + recipes.push(pumpkin_nbt::tag::NbtTag::Compound(recipe)); + } + let mut offers_compound = NbtCompound::new(); + offers_compound.put("Recipes", pumpkin_nbt::tag::NbtTag::List(recipes)); + nbt.put_compound("Offers", offers_compound); + }; + // Inventory let inventory = self.inventory.lock().await; let mut inventory_list = Vec::new(); @@ -359,6 +509,7 @@ impl NBTStorage for VillagerEntity { }) } + #[allow(clippy::too_many_lines)] fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> crate::entity::NbtFuture<'a, ()> { Box::pin(async move { self.mob_entity @@ -369,14 +520,13 @@ impl NBTStorage for VillagerEntity { if let Some(villager_data_nbt) = nbt.get_compound("VillagerData") { let mut data = self.villager_data.lock().await; if let Some(t) = villager_data_nbt.get_int("Type") { - data.r#type = VillagerType::try_from(t).unwrap_or(VillagerType::Plains); + data.r#type = VarInt(t); } if let Some(p) = villager_data_nbt.get_int("Profession") { - data.profession = - VillagerProfession::try_from(p).unwrap_or(VillagerProfession::None); + data.profession = VarInt(p); } if let Some(l) = villager_data_nbt.get_int("Level") { - data.level = l; + data.level = VarInt(l); } } @@ -393,6 +543,70 @@ impl NBTStorage for VillagerEntity { self.restocks_today.store(today, Ordering::Relaxed); } + if let (Some(x), Some(y), Some(z)) = ( + nbt.get_int("JobSiteX"), + nbt.get_int("JobSiteY"), + nbt.get_int("JobSiteZ"), + ) { + *self.job_site.lock().unwrap() = Some(BlockPos::new(x, y, z)); + } else { + *self.job_site.lock().unwrap() = None; + } + + if let (Some(x), Some(y), Some(z)) = ( + nbt.get_int("HomeX").or_else(|| nbt.get_int("BedX")), + nbt.get_int("HomeY").or_else(|| nbt.get_int("BedY")), + nbt.get_int("HomeZ").or_else(|| nbt.get_int("BedZ")), + ) { + *self.home_pos.lock().unwrap() = Some(BlockPos::new(x, y, z)); + } else { + *self.home_pos.lock().unwrap() = None; + } + + if let Some(offers_compound) = nbt.get_compound("Offers") + && let Some(recipes) = offers_compound.get_list("Recipes") + { + let mut offers = self.offers.lock().await; + offers.clear(); + for tag in recipes { + if let Some(recipe) = tag.extract_compound() { + let buy = recipe + .get_compound("buy") + .and_then(ItemStack::read_item_stack); + let buy_b = recipe + .get_compound("buyB") + .and_then(ItemStack::read_item_stack); + let sell_item = recipe + .get_compound("sell") + .and_then(ItemStack::read_item_stack); + + if let (Some(buy), Some(sell_item)) = (buy, sell_item) { + let uses = recipe.get_int("uses").unwrap_or(0); + let max_uses = recipe.get_int("maxUses").unwrap_or(12); + let reward_exp = recipe.get_bool("rewardExp").unwrap_or(true); + let xp = recipe.get_int("xp").unwrap_or(2); + let price_multiplier = + recipe.get_float("priceMultiplier").unwrap_or(0.05); + let special_price = recipe.get_int("specialPrice").unwrap_or(0); + let demand = recipe.get_int("demand").unwrap_or(0); + + offers.push(pumpkin_protocol::java::client::play::MerchantOffer { + base_cost_a: buy.into(), + output: sell_item.into(), + cost_b: buy_b.map(Into::into), + is_disabled: !reward_exp, + uses, + max_uses, + xp, + special_price, + price_multiplier, + demand, + }); + } + } + } + } + // Inventory if let Some(inventory_list) = nbt.get_list("Inventory") { let mut inventory = self.inventory.lock().await; @@ -442,11 +656,349 @@ impl NBTStorage for VillagerEntity { } } +fn block_to_profession(block: &Block) -> Option { + if block == &Block::COMPOSTER { + Some(VillagerProfession::Farmer) + } else if block == &Block::LECTERN { + Some(VillagerProfession::Librarian) + } else if block == &Block::BLAST_FURNACE { + Some(VillagerProfession::Armorer) + } else if block == &Block::SMOKER { + Some(VillagerProfession::Butcher) + } else if block == &Block::CARTOGRAPHY_TABLE { + Some(VillagerProfession::Cartographer) + } else if block == &Block::BREWING_STAND { + Some(VillagerProfession::Cleric) + } else if block == &Block::BARREL { + Some(VillagerProfession::Fisherman) + } else if block == &Block::FLETCHING_TABLE { + Some(VillagerProfession::Fletcher) + } else if block == &Block::CAULDRON + || block == &Block::WATER_CAULDRON + || block == &Block::LAVA_CAULDRON + || block == &Block::POWDER_SNOW_CAULDRON + { + Some(VillagerProfession::Leatherworker) + } else if block == &Block::STONECUTTER { + Some(VillagerProfession::Mason) + } else if block == &Block::LOOM { + Some(VillagerProfession::Shepherd) + } else if block == &Block::SMITHING_TABLE { + Some(VillagerProfession::Toolsmith) + } else if block == &Block::GRINDSTONE { + Some(VillagerProfession::Weaponsmith) + } else { + None + } +} + +fn profession_matches_block(profession: VillagerProfession, block: &Block) -> bool { + match profession { + VillagerProfession::Farmer => block == &Block::COMPOSTER, + VillagerProfession::Librarian => block == &Block::LECTERN, + VillagerProfession::Armorer => block == &Block::BLAST_FURNACE, + VillagerProfession::Butcher => block == &Block::SMOKER, + VillagerProfession::Cartographer => block == &Block::CARTOGRAPHY_TABLE, + VillagerProfession::Cleric => block == &Block::BREWING_STAND, + VillagerProfession::Fisherman => block == &Block::BARREL, + VillagerProfession::Fletcher => block == &Block::FLETCHING_TABLE, + VillagerProfession::Leatherworker => { + block == &Block::CAULDRON + || block == &Block::WATER_CAULDRON + || block == &Block::LAVA_CAULDRON + || block == &Block::POWDER_SNOW_CAULDRON + } + VillagerProfession::Mason => block == &Block::STONECUTTER, + VillagerProfession::Shepherd => block == &Block::LOOM, + VillagerProfession::Toolsmith => block == &Block::SMITHING_TABLE, + VillagerProfession::Weaponsmith => block == &Block::GRINDSTONE, + _ => false, + } +} + impl Mob for VillagerEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + fn get_job_site(&self) -> Option { + *self.job_site.lock().unwrap() + } + + fn get_home(&self) -> Option { + *self.home_pos.lock().unwrap() + } + + #[expect(clippy::too_many_lines)] + fn mob_tick<'a>( + &'a self, + _caller: &'a Arc, + ) -> crate::entity::EntityBaseFuture<'a, ()> { + Box::pin(async move { + let age = self.get_entity().age.load(Ordering::Relaxed); + if age % 20 != 0 { + return; + } + + let world = self.get_entity().world.load(); + + // 1. Bed / Sleeping logic (for all villagers: babies, nitwits, adults) + let is_sleeping = self.get_entity().pose.load() == EntityPose::Sleeping; + + // Check if current bed is still valid + if let Some(current_home) = self.get_home_pos() { + let (block, state) = world.get_block_and_state(¤t_home); + let valid = if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) { + let bed_props = BedProperties::from_state_id(state.id, block); + bed_props.part == BedPart::Head + } else { + false + }; + + if !valid { + *self.home_pos.lock().unwrap() = None; + if is_sleeping { + // Wake up if bed was broken + self.get_entity().set_pose(EntityPose::Standing); + self.get_entity().send_meta_data(&[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + None::, + )]); + } + } + } + + // If no bed, search for one + if self.get_home_pos().is_none() { + let pos = self.get_entity().block_pos.load(); + let start = BlockPos::new(pos.0.x - 16, pos.0.y - 4, pos.0.z - 16); + let end = BlockPos::new(pos.0.x + 16, pos.0.y + 4, pos.0.z + 16); + + let aabb = BoundingBox::new( + Vector3::new( + pos.0.x as f64 - 32.0, + pos.0.y as f64 - 16.0, + pos.0.z as f64 - 32.0, + ), + Vector3::new( + pos.0.x as f64 + 32.0, + pos.0.y as f64 + 16.0, + pos.0.z as f64 + 32.0, + ), + ); + let nearby_entities = world.get_all_at_box(&aabb); + + let mut claimed_homes = Vec::new(); + for entity in nearby_entities { + if entity.get_entity().entity_id != self.get_entity().entity_id + && entity.get_entity().entity_type + == &pumpkin_data::entity::EntityType::VILLAGER + && let Some(home) = entity.get_home_pos() + { + claimed_homes.push(home); + } + } + + let mut best_home = None; + let mut best_dist = f64::MAX; + + for p in BlockPos::iterate(start, end) { + let (block, state) = world.get_block_and_state(&p); + if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) { + let bed_props = BedProperties::from_state_id(state.id, block); + let bed_head_pos = if bed_props.part == BedPart::Head { + p + } else { + p.offset(bed_props.facing.to_offset()) + }; + + if claimed_homes.contains(&bed_head_pos) { + continue; + } + + let dist = bed_head_pos + .to_f64() + .squared_distance_to_vec(&self.get_entity().pos.load()); + if dist < best_dist { + best_dist = dist; + best_home = Some(bed_head_pos); + } + } + } + + if let Some(home) = best_home { + *self.home_pos.lock().unwrap() = Some(home); + } + } + + // Handle Sleeping/Waking up based on time + let is_sleeping = self.get_entity().pose.load() == EntityPose::Sleeping; + if let Some(home_pos) = self.get_home_pos() { + let time = world.level_time.lock().await.time_of_day; + let is_night = (12000..=23000).contains(&time); + + if is_night { + if !is_sleeping { + // Check distance to bed. If close enough, go to sleep + let dist = home_pos + .to_f64() + .squared_distance_to_vec(&self.get_entity().pos.load()); + if dist <= 4.0 { + // Within 2 blocks (squared distance 4.0) + let (block, state) = world.get_block_and_state(&home_pos); + if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) { + let bed_props = BedProperties::from_state_id(state.id, block); + if !bed_props.occupied { + // Make bed occupied + BedBlock::set_occupied( + true, &world, block, &home_pos, state.id, + ) + .await; + + self.get_entity().set_pose(EntityPose::Sleeping); + self.get_entity().send_meta_data(&[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + Some(home_pos), + )]); + } + } + } + } + } else if is_sleeping { + // It is day, wake up! + let (block, state) = world.get_block_and_state(&home_pos); + if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_BEDS) { + let bed_props = BedProperties::from_state_id(state.id, block); + if bed_props.occupied { + BedBlock::set_occupied(false, &world, block, &home_pos, state.id).await; + } + } + + self.get_entity().set_pose(EntityPose::Standing); + self.get_entity().send_meta_data(&[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + None::, + )]); + } + } + + // 2. Job / Profession logic (skip for Nitwits and babies) + let data = self.villager_data.lock().await; + let is_adult = self.get_entity().age.load(Ordering::Relaxed) >= 0; + let xp = self.xp.load(Ordering::Relaxed); + let profession = data.profession_enum(); + drop(data); + + if profession == VillagerProfession::Nitwit || !is_adult { + return; + } + + if let Some(current_site) = self.get_job_site() { + let (block, _state) = world.get_block_and_state(¤t_site); + let valid = if profession == VillagerProfession::None { + block_to_profession(block).is_some() + } else { + profession_matches_block(profession, block) + }; + + if !valid { + *self.job_site.lock().unwrap() = None; + if xp == 0 && profession != VillagerProfession::None { + let r#type = self.villager_data.lock().await.type_enum(); + self.set_villager_data(VillagerData::new( + r#type, + VillagerProfession::None, + 1, + )) + .await; + self.offers.lock().await.clear(); + } + } + } + + if self.get_job_site().is_none() { + let pos = self.get_entity().block_pos.load(); + let start = BlockPos::new(pos.0.x - 10, pos.0.y - 4, pos.0.z - 10); + let end = BlockPos::new(pos.0.x + 10, pos.0.y + 4, pos.0.z + 10); + + let aabb = BoundingBox::new( + Vector3::new( + pos.0.x as f64 - 32.0, + pos.0.y as f64 - 16.0, + pos.0.z as f64 - 32.0, + ), + Vector3::new( + pos.0.x as f64 + 32.0, + pos.0.y as f64 + 16.0, + pos.0.z as f64 + 32.0, + ), + ); + let nearby_entities = world.get_all_at_box(&aabb); + + let mut claimed_sites = Vec::new(); + for entity in nearby_entities { + if entity.get_entity().entity_id != self.get_entity().entity_id + && entity.get_entity().entity_type + == &pumpkin_data::entity::EntityType::VILLAGER + && let Some(site) = entity.get_job_site_pos() + { + claimed_sites.push(site); + } + } + + let mut best_site = None; + let mut best_dist = f64::MAX; + let mut best_profession = VillagerProfession::None; + + for p in BlockPos::iterate(start, end) { + if claimed_sites.contains(&p) { + continue; + } + + let (block, _state) = world.get_block_and_state(&p); + if let Some(prof) = block_to_profession(block) { + if profession != VillagerProfession::None && prof != profession { + continue; + } + + let dist = p + .to_f64() + .squared_distance_to_vec(&self.get_entity().pos.load()); + if dist < best_dist { + best_dist = dist; + best_site = Some(p); + best_profession = prof; + } + } + } + + if let Some(site) = best_site { + *self.job_site.lock().unwrap() = Some(site); + if profession == VillagerProfession::None { + let r#type = self.villager_data.lock().await.type_enum(); + self.set_villager_data(VillagerData::new(r#type, best_profession, 1)) + .await; + } + } + } else { + let current_prof = self.villager_data.lock().await.profession_enum(); + if current_prof == VillagerProfession::None + && let Some(site) = self.get_job_site() + { + let (block, _state) = world.get_block_and_state(&site); + if let Some(prof) = block_to_profession(block) { + let r#type = self.villager_data.lock().await.type_enum(); + self.set_villager_data(VillagerData::new(r#type, prof, 1)) + .await; + } + } + } + }) + } + fn mob_interact<'a>( &'a self, player: &'a Arc, @@ -459,7 +1011,23 @@ impl Mob for VillagerEntity { return true; } - let offers = self.offers.lock().await; + let mut offers = self.offers.lock().await; + if offers.is_empty() { + let data = self.villager_data.lock().await; + if data.profession_enum() != VillagerProfession::None + && data.profession_enum() != VillagerProfession::Nitwit + { + let prof = data.profession_enum(); + let level = data.level.0; + drop(data); + drop(offers); + self.generate_trades(prof, level).await; + offers = self.offers.lock().await; + } else { + drop(data); + } + } + if offers.is_empty() { self.set_unhappy(); return true; @@ -474,7 +1042,7 @@ impl Mob for VillagerEntity { ) .await; - self.open_trading_screen(&player); + self.open_trading_screen(&player).await; true }) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 3cee015a9..5eee09ac3 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -4796,6 +4796,10 @@ impl MessageCache { } impl InventoryPlayer for Player { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn drop_item(&self, item: ItemStack, _retain_ownership: bool) -> PlayerFuture<'_, ()> { Box::pin(async move { self.drop_item(item).await; diff --git a/pumpkin/src/entity/vehicle/minecart.rs b/pumpkin/src/entity/vehicle/minecart.rs index 946413ad2..e65f9a023 100644 --- a/pumpkin/src/entity/vehicle/minecart.rs +++ b/pumpkin/src/entity/vehicle/minecart.rs @@ -150,13 +150,11 @@ impl EntityBase for MinecartEntity { &pos, ); } - } else { - if self.vehicle.get_hurt_time() == 0 { - self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); - self.vehicle.set_hurt_time(10); - self.vehicle.set_damage(50.0); - // TODO: Send entity status - } + } else if self.vehicle.get_hurt_time() == 0 { + self.vehicle.set_hurt_dir(-self.vehicle.get_hurt_dir()); + self.vehicle.set_hurt_time(10); + self.vehicle.set_damage(50.0); + // TODO: Send entity status } } } else if is_powered_rail { diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index d5b2e664b..ff9e55f02 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -1775,13 +1775,15 @@ impl JavaClient { } ActionType::Interact | ActionType::InteractAt => { let held = player.inventory.held_item(); - let mut stack = held.lock().await; - if !event.target.interact(player, &mut stack).await { + let mut stack = held.lock().await.clone(); + let interacted = event.target.interact(player, &mut stack).await; + if !interacted { server .item_registry .use_on_entity(&mut stack, player, event.target) .await; } + *held.lock().await = stack; } } }