feat: villager Professions

This commit is contained in:
Alexander Medvedev 2026-06-21 21:54:10 +02:00
parent daa0111009
commit 5f60d23804
14 changed files with 978 additions and 86 deletions

View File

@ -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}'"),
};

View File

@ -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<Box<dyn DataCompone
DataComponent::ChargedProjectiles => {
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<crate::item_stack::ItemStack>,
}
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<Self> {
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::<BundleContentsImpl>().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<crate::item_stack::ItemStack> {
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<String>,
}
impl WritableBookContentImpl {
pub fn read_data(tag: &NbtTag) -> Option<Self> {
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<String>,
}
impl WrittenBookContentImpl {
pub fn read_data(tag: &NbtTag) -> Option<Self> {
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<Self> {
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<Self> {
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)]

View File

@ -18,6 +18,7 @@ pub struct MerchantScreenHandler {
behaviour: ScreenHandlerBehaviour,
selected_offer: usize,
pub offers: Vec<pumpkin_protocol::java::client::play::MerchantOffer>,
pub on_trade: Option<Box<dyn Fn(usize) + Send + Sync>>,
}
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);
}
}
}
}

View File

@ -121,6 +121,7 @@ pub type PlayerFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 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::<pumpkin_data::data_component_impl::BundleContentsImpl>()
&& bundle.try_insert(&mut cursor_stack) {
intercepted = true;
}
}
if !intercepted && !slot_stack.is_empty()
&& let Some(bundle) = cursor_stack.get_data_component_mut::<pumpkin_data::data_component_impl::BundleContentsImpl>() {
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::<pumpkin_data::data_component_impl::BundleContentsImpl>()
&& 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::<pumpkin_data::data_component_impl::BundleContentsImpl>()
&& 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::<EquippableImpl>()
.map_or(&EquipmentSlot::MAIN_HAND, |equippable| equippable.slot);

View File

@ -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<ItemStackSerializer<'static>>,
pub cost_b: Option<ItemStackSerializer<'static>>, // TODO: item cost
pub is_disabled: bool,
pub uses: i32,
pub max_uses: i32,

View File

@ -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<u8>> {
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::<LecternBlockEntity>()
{
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::<pumpkin_data::data_component_impl::WrittenBookContentImpl>() {
pages = comp.pages.len().max(1) as f32;
} else if let Some(comp) = book_guard.get_data_component::<pumpkin_data::data_component_impl::WritableBookContentImpl>() {
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)
}
})
}
}

View File

@ -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<Mutex<ItemStack>>,
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),
}
}

View File

@ -397,6 +397,14 @@ pub trait Mob: EntityBase + Send + Sync {
fn get_mob_entity(&self) -> &MobEntity;
fn get_job_site(&self) -> Option<BlockPos> {
None
}
fn get_home(&self) -> Option<BlockPos> {
None
}
fn get_path_aware_entity(&self) -> Option<&dyn PathAwareEntity> {
None
}
@ -675,6 +683,14 @@ impl<T: Mob + Send + 'static> EntityBase for T {
.is_some_and(PathAwareEntity::is_panicking)
}
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
<T as Mob>::get_job_site(self)
}
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
<T as Mob>::get_home(self)
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}

View File

@ -147,6 +147,14 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
})
}
fn get_job_site_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
None
}
fn get_home_pos(&self) -> Option<pumpkin_util::math::position::BlockPos> {
None
}
fn as_any(&self) -> &dyn std::any::Any
where
Self: Sized,

View File

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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)
}
}

View File

@ -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<Mutex<Vec<Arc<Mutex<ItemStack>>>>>,
pub merchant_inventory: Arc<SimpleInventory>,
pub offers: Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>,
pub job_site: std::sync::Mutex<Option<BlockPos>>,
pub home_pos: std::sync::Mutex<Option<BlockPos>>,
pub self_weak: std::sync::Mutex<Option<Weak<Self>>>,
}
impl VillagerEntity {
pub fn new(entity: Entity) -> Arc<Self> {
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<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = 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<Player>) {
// 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<Player>) {
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<pumpkin_inventory::player::player_inventory::PlayerInventory>,
_player: &'a dyn InventoryPlayer,
player: &'a dyn InventoryPlayer,
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
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::<crate::entity::player::Player>()
.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<VillagerProfession> {
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<BlockPos> {
*self.job_site.lock().unwrap()
}
fn get_home(&self) -> Option<BlockPos> {
*self.home_pos.lock().unwrap()
}
#[expect(clippy::too_many_lines)]
fn mob_tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
) -> 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(&current_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::<BlockPos>,
)]);
}
}
}
// 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::<BlockPos>,
)]);
}
}
// 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(&current_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<Player>,
@ -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
})

View File

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

View File

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

View File

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