feat: add rudimentary lectern

This commit is contained in:
Alexander Medvedev 2026-05-01 20:01:40 +02:00
parent 44270fbde1
commit 30f4d4e70e
3 changed files with 279 additions and 1 deletions

View File

@ -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<Mutex<ItemStack>>,
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<Box<dyn Future<Output = ()> + 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<Self>) -> Option<Arc<dyn Inventory>> {
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<Mutex<ItemStack>>> {
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<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
*self.book.lock().await = ItemStack::EMPTY.clone();
self.mark_dirty();
})
}
}

View File

@ -23,6 +23,7 @@ use crate::block::entities::command_block::CommandBlockEntity;
use crate::block::entities::ender_chest::EnderChestBlockEntity; use crate::block::entities::ender_chest::EnderChestBlockEntity;
use crate::block::entities::hopper::HopperBlockEntity; use crate::block::entities::hopper::HopperBlockEntity;
use crate::block::entities::jukebox::JukeboxBlockEntity; use crate::block::entities::jukebox::JukeboxBlockEntity;
use crate::block::entities::lectern::LecternBlockEntity;
use crate::block::entities::mob_spawner::MobSpawnerBlockEntity; use crate::block::entities::mob_spawner::MobSpawnerBlockEntity;
use crate::block::entities::shulker_box::ShulkerBoxBlockEntity; use crate::block::entities::shulker_box::ShulkerBoxBlockEntity;
use crate::block::entities::smoker::SmokerBlockEntity; use crate::block::entities::smoker::SmokerBlockEntity;
@ -49,6 +50,7 @@ pub mod furnace;
pub mod furnace_like_block_entity; pub mod furnace_like_block_entity;
pub mod hopper; pub mod hopper;
pub mod jukebox; pub mod jukebox;
pub mod lectern;
pub mod mob_spawner; pub mod mob_spawner;
pub mod piston; pub mod piston;
pub mod shulker_box; pub mod shulker_box;
@ -190,6 +192,7 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option<Arc<dyn BlockEntity>>
Arc::new(block_entity_from_generic::<BrewingStandBlockEntity>(nbt)) Arc::new(block_entity_from_generic::<BrewingStandBlockEntity>(nbt))
} }
BellBlockEntity::ID => Arc::new(block_entity_from_generic::<BellBlockEntity>(nbt)), BellBlockEntity::ID => Arc::new(block_entity_from_generic::<BellBlockEntity>(nbt)),
LecternBlockEntity::ID => Arc::new(block_entity_from_generic::<LecternBlockEntity>(nbt)),
_ => return None, _ => return None,
}) })
} }

View File

@ -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::block_properties::{BlockProperties, LecternLikeProperties};
use pumpkin_data::entity::EntityType;
use pumpkin_macros::pumpkin_block; use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::BlockStateId; 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")] #[pumpkin_block("minecraft:lectern")]
pub struct LecternBlock; pub struct LecternBlock;
impl LecternBlock {
async fn update_lectern_state(
has_book: bool,
block: &Block,
position: &BlockPos,
world: &Arc<World>,
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 { 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> { fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move { Box::pin(async move {
let mut props = LecternLikeProperties::default(args.block); let mut props = LecternLikeProperties::default(args.block);
@ -19,4 +57,88 @@ impl BlockBehaviour for LecternBlock {
props.to_state_id(args.block) 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::<LecternBlockEntity>()
{
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::<LecternBlockEntity>()
&& 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::<LecternBlockEntity>()
{
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;
}
}
})
}
} }