diff --git a/pumpkin-world/src/block/entities/lectern.rs b/pumpkin-world/src/block/entities/lectern.rs new file mode 100644 index 000000000..d4fe81375 --- /dev/null +++ b/pumpkin-world/src/block/entities/lectern.rs @@ -0,0 +1,153 @@ +use pumpkin_data::item_stack::ItemStack; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::position::BlockPos; +use std::{ + any::Any, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; +use tokio::sync::Mutex; + +use crate::{ + block::entities::BlockEntity, + inventory::{Clearable, Inventory, InventoryFuture}, +}; + +pub struct LecternBlockEntity { + pub position: BlockPos, + pub book: Arc>, + pub dirty: AtomicBool, +} + +impl BlockEntity for LecternBlockEntity { + fn resource_location(&self) -> &'static str { + Self::ID + } + + fn get_position(&self) -> BlockPos { + self.position + } + + fn from_nbt(nbt: &NbtCompound, position: BlockPos) -> Self + where + Self: Sized, + { + let book = nbt + .get_compound("Book") + .and_then(ItemStack::read_item_stack) + .map(|stack| Arc::new(Mutex::new(stack))) + .unwrap_or_else(|| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))); + + Self { + position, + book, + dirty: AtomicBool::new(false), + } + } + + fn write_nbt<'a>( + &'a self, + nbt: &'a mut NbtCompound, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let book = self.book.lock().await; + if !book.is_empty() { + let mut book_nbt = NbtCompound::default(); + book.write_item_stack(&mut book_nbt); + nbt.put_compound("Book", book_nbt); + } + }) + } + + fn get_inventory(self: Arc) -> Option> { + Some(self) + } + + fn is_dirty(&self) -> bool { + self.dirty.load(Ordering::Relaxed) + } + + fn clear_dirty(&self) { + self.dirty.store(false, Ordering::Relaxed); + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +impl LecternBlockEntity { + pub const ID: &'static str = "minecraft:lectern"; + + #[must_use] + pub fn new(position: BlockPos) -> Self { + Self { + position, + book: Arc::new(Mutex::new(ItemStack::EMPTY.clone())), + dirty: AtomicBool::new(false), + } + } +} + +impl Inventory for LecternBlockEntity { + fn size(&self) -> usize { + 1 + } + + fn is_empty(&self) -> InventoryFuture<'_, bool> { + Box::pin(async move { self.book.lock().await.is_empty() }) + } + + fn get_stack(&self, _slot: usize) -> InventoryFuture<'_, Arc>> { + Box::pin(async move { self.book.clone() }) + } + + fn remove_stack(&self, _slot: usize) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut removed = ItemStack::EMPTY.clone(); + let mut guard = self.book.lock().await; + std::mem::swap(&mut removed, &mut *guard); + self.mark_dirty(); + removed + }) + } + + fn remove_stack_specific(&self, _slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> { + Box::pin(async move { + let mut stack = self.book.lock().await; + if stack.is_empty() { + return ItemStack::EMPTY.clone(); + } + let res = stack.split(amount); + self.mark_dirty(); + res + }) + } + + fn set_stack(&self, _slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> { + Box::pin(async move { + *self.book.lock().await = stack; + self.mark_dirty(); + }) + } + + fn mark_dirty(&self) { + self.dirty.store(true, Ordering::Relaxed); + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl Clearable for LecternBlockEntity { + fn clear(&self) -> Pin + Send + '_>> { + Box::pin(async move { + *self.book.lock().await = ItemStack::EMPTY.clone(); + self.mark_dirty(); + }) + } +} diff --git a/pumpkin-world/src/block/entities/mod.rs b/pumpkin-world/src/block/entities/mod.rs index ea1626dfb..ba6323dde 100644 --- a/pumpkin-world/src/block/entities/mod.rs +++ b/pumpkin-world/src/block/entities/mod.rs @@ -23,6 +23,7 @@ use crate::block::entities::command_block::CommandBlockEntity; use crate::block::entities::ender_chest::EnderChestBlockEntity; use crate::block::entities::hopper::HopperBlockEntity; use crate::block::entities::jukebox::JukeboxBlockEntity; +use crate::block::entities::lectern::LecternBlockEntity; use crate::block::entities::mob_spawner::MobSpawnerBlockEntity; use crate::block::entities::shulker_box::ShulkerBoxBlockEntity; use crate::block::entities::smoker::SmokerBlockEntity; @@ -49,6 +50,7 @@ pub mod furnace; pub mod furnace_like_block_entity; pub mod hopper; pub mod jukebox; +pub mod lectern; pub mod mob_spawner; pub mod piston; pub mod shulker_box; @@ -190,6 +192,7 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option> Arc::new(block_entity_from_generic::(nbt)) } BellBlockEntity::ID => Arc::new(block_entity_from_generic::(nbt)), + LecternBlockEntity::ID => Arc::new(block_entity_from_generic::(nbt)), _ => return None, }) } diff --git a/pumpkin/src/block/blocks/lectern.rs b/pumpkin/src/block/blocks/lectern.rs index 7aa205ae8..d79b310a5 100644 --- a/pumpkin/src/block/blocks/lectern.rs +++ b/pumpkin/src/block/blocks/lectern.rs @@ -1,12 +1,50 @@ -use crate::block::{BlockBehaviour, BlockFuture, OnPlaceArgs}; +use std::sync::Arc; + +use crate::block::registry::BlockActionResult; +use crate::block::{ + BlockBehaviour, BlockFuture, BrokenArgs, NormalUseArgs, OnPlaceArgs, PlacedArgs, + UseWithItemArgs, +}; +use crate::entity::Entity; +use crate::entity::item::ItemEntity; +use crate::world::World; +use pumpkin_data::Block; use pumpkin_data::block_properties::{BlockProperties, LecternLikeProperties}; +use pumpkin_data::entity::EntityType; use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; use pumpkin_world::BlockStateId; +use pumpkin_world::block::entities::lectern::LecternBlockEntity; +use pumpkin_world::inventory::Inventory; +use pumpkin_world::world::BlockFlags; #[pumpkin_block("minecraft:lectern")] pub struct LecternBlock; +impl LecternBlock { + async fn update_lectern_state( + has_book: bool, + block: &Block, + position: &BlockPos, + world: &Arc, + props: &mut LecternLikeProperties, + ) { + props.has_book = has_book; + world + .set_block_state(position, props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + } +} + impl BlockBehaviour for LecternBlock { + fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + let block_entity = LecternBlockEntity::new(*args.position); + args.world.add_block_entity(Arc::new(block_entity)).await; + }) + } + fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> { Box::pin(async move { let mut props = LecternLikeProperties::default(args.block); @@ -19,4 +57,88 @@ impl BlockBehaviour for LecternBlock { props.to_state_id(args.block) }) } + + fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + if let Some(block_entity) = args.world.get_block_entity(args.position).await + && let Some(lectern_entity) = + block_entity.as_any().downcast_ref::() + { + let book = lectern_entity.remove_stack(0).await; + if !book.is_empty() { + // Logic to give the book to the player + // Need to find a proper way to give items to player. For now skip. + + let mut props = LecternLikeProperties::from_state_id( + args.world.get_block_state(args.position).await.id, + args.block, + ); + Self::update_lectern_state( + false, + args.block, + args.position, + args.world, + &mut props, + ) + .await; + return BlockActionResult::Success; + } + } + BlockActionResult::Pass + }) + } + + fn use_with_item<'a>( + &'a self, + args: UseWithItemArgs<'a>, + ) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + let mut item_stack = args.item_stack.lock().await; + + // Check if it's a book + if item_stack.item.registry_key.contains("book") + && let Some(block_entity) = args.world.get_block_entity(args.position).await + && let Some(lectern_entity) = + block_entity.as_any().downcast_ref::() + && lectern_entity.book.lock().await.is_empty() + { + let book = item_stack.split_unless_creative(args.player.gamemode.load(), 1); + lectern_entity.set_stack(0, book).await; + + let mut props = LecternLikeProperties::from_state_id( + args.world.get_block_state(args.position).await.id, + args.block, + ); + Self::update_lectern_state(true, args.block, args.position, args.world, &mut props) + .await; + return BlockActionResult::Success; + } + BlockActionResult::Pass + }) + } + + fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { + Box::pin(async move { + if let Some(block_entity) = args.world.get_block_entity(args.position).await + && let Some(lectern_entity) = + block_entity.as_any().downcast_ref::() + { + let book = lectern_entity.remove_stack(0).await; + if !book.is_empty() { + // Drop the book item + let entity = Entity::new( + args.world.clone(), + Vector3::new( + f64::from(args.position.0.x) + 0.5, + f64::from(args.position.0.y) + 0.5, + f64::from(args.position.0.z) + 0.5, + ), + &EntityType::ITEM, + ); + let item_entity = ItemEntity::new(entity, book).await; + args.world.spawn_entity(Arc::new(item_entity)).await; + } + } + }) + } }