Pumpkin/crates/pumpkin/src/block/entities/chest_like_block_entity.rs

376 lines
15 KiB
Rust

/// Implements the `BlockEntity` trait for chest-like block entities.
/// Parameters:
/// - $`struct_name`: The type of the chest struct (e.g., `ChestBlockEntity`)
/// - $`resource_id`: The resource location string (e.g., "minecraft:chest")
#[macro_export]
macro_rules! impl_block_entity_for_chest {
($struct_name:ty) => {
impl $crate::block::entities::BlockEntity for $struct_name {
fn resource_location(&self) -> &'static str {
Self::ID
}
fn get_position(&self) -> BlockPos {
self.position
}
fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self
where
Self: Sized,
{
use pumpkin_world::inventory::Inventory;
// Read deferred loot-table fields first.
let loot_table_key = nbt.get_string("LootTable").map(|s| s.to_string());
let loot_table_seed = nbt.get_long("LootTableSeed").unwrap_or(0);
let chest = Self {
position,
items: tokio::sync::RwLock::new(std::array::from_fn(|_| ItemStack::EMPTY.clone())),
dirty: std::sync::atomic::AtomicBool::new(false),
viewers: $crate::block::viewer::ViewerCountTracker::new(),
loot_table: StdMutex::new(loot_table_key),
loot_table_seed,
};
// Only read saved items when there is no pending loot table.
if chest.loot_table.lock().expect("Loot table mutex should not be poisoned").is_none() {
chest.read_data(nbt, &mut *chest.items.blocking_write());
}
chest
}
fn write_nbt<'a>(
&'a self,
nbt: &'a mut pumpkin_nbt::compound::NbtCompound,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
use pumpkin_world::inventory::Inventory;
Box::pin(async move {
// Clone the loot table key without holding the lock across an await.
let loot_table_key = {
let guard = self.loot_table.lock().expect("Loot table mutex should not be poisoned");
guard.clone()
};
if let Some(key) = loot_table_key {
// Persist deferred loot: write the key and seed; skip items.
nbt.put_string("LootTable", key);
if self.loot_table_seed != 0 {
nbt.put_long("LootTableSeed", self.loot_table_seed);
}
} else {
// Loot has already been generated, so persist the actual items.
self.write_inventory_nbt(nbt, true).await;
}
})
}
fn tick<'a>(
&'a self,
world: &'a Arc<$crate::world::World>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
$crate::block::viewer::ViewerCountTrackerExt::update_viewer_count::<$struct_name>(
&self.viewers,
self,
world,
&self.position,
)
.await;
})
}
fn get_inventory(self: Arc<Self>) -> Option<Arc<dyn pumpkin_world::inventory::Inventory>> {
Some(self)
}
fn is_dirty(&self) -> bool {
self.dirty.load(std::sync::atomic::Ordering::Relaxed)
}
fn clear_dirty(&self) {
self.dirty
.store(false, std::sync::atomic::Ordering::Relaxed);
}
fn chunk_data_nbt(&self) -> Option<pumpkin_nbt::compound::NbtCompound> {
let mut nbt = pumpkin_nbt::compound::NbtCompound::new();
let loot_table_key = self.loot_table.lock().expect("Loot table mutex should not be poisoned").clone();
if let Some(key) = loot_table_key {
nbt.put_string("LootTable", key);
if self.loot_table_seed != 0 {
nbt.put_long("LootTableSeed", self.loot_table_seed);
}
} else {
let items = futures::executor::block_on(self.items.read());
pumpkin_world::inventory::sync_write_items_to_nbt(&*items, &mut nbt);
}
Some(nbt)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn take_loot_table(&self) -> Option<(String, i64)> {
let mut guard = self.loot_table.lock().expect("Loot table mutex should not be poisoned");
guard.take().map(|key| (key, self.loot_table_seed))
}
fn has_loot_table(&self) -> bool {
self.loot_table.lock().expect("Loot table mutex should not be poisoned").is_some()
}
}
};
}
/// Implements the Inventory trait for chest-like block entities.
#[macro_export]
macro_rules! impl_inventory_for_chest {
($struct_name:ty) => {
impl pumpkin_world::inventory::Inventory for $struct_name {
fn size(&self) -> usize {
Self::INVENTORY_SIZE
}
fn is_empty(&self) -> pumpkin_world::inventory::InventoryFuture<'_, bool> {
Box::pin(async move {
let items = self.items.read().await;
items.iter().all(|s| s.is_empty())
})
}
fn get_stack(
&self,
slot: usize,
) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
Box::pin(async move {
let items = self.items.read().await;
items[slot].clone()
})
}
fn remove_stack(
&self,
slot: usize,
) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
Box::pin(async move {
let mut items = self.items.write().await;
let removed = std::mem::replace(&mut items[slot], ItemStack::EMPTY.clone());
self.mark_dirty();
removed
})
}
fn remove_stack_specific(
&self,
slot: usize,
amount: u8,
) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
Box::pin(async move {
let mut items = self.items.write().await;
let res = if !items[slot].is_empty() && amount > 0 {
items[slot].split(amount)
} else {
ItemStack::EMPTY.clone()
};
self.mark_dirty();
res
})
}
fn set_stack(
&self,
slot: usize,
stack: ItemStack,
) -> pumpkin_world::inventory::InventoryFuture<'_, ()> {
Box::pin(async move {
let mut items = self.items.write().await;
items[slot] = stack;
self.mark_dirty();
})
}
fn on_open(&self) -> pumpkin_world::inventory::InventoryFuture<'_, ()> {
Box::pin(async move {
self.viewers.open_container();
})
}
fn on_close(&self) -> pumpkin_world::inventory::InventoryFuture<'_, ()> {
Box::pin(async move {
self.viewers.close_container();
})
}
fn mark_dirty(&self) {
self.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
};
}
/// Implements the Clearable trait for chest-like block entities.
#[macro_export]
macro_rules! impl_clearable_for_chest {
($struct_name:ty) => {
impl pumpkin_world::inventory::Clearable for $struct_name {
fn clear(
&self,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let mut items = self.items.write().await;
items.fill_with(|| ItemStack::EMPTY.clone());
<$struct_name as pumpkin_world::inventory::Inventory>::mark_dirty(self);
})
}
}
};
}
/// Implements the `ViewerCountListener` trait for chest-like block entities.
///
/// The behavior is controlled by the `EMITS_REDSTONE` constant on the struct.
/// When `EMITS_REDSTONE` is true, updates neighbors for redstone signals when viewer count changes.
#[macro_export]
macro_rules! impl_viewer_count_listener_for_chest {
($struct_name:ty) => {
impl $crate::block::viewer::ViewerCountListener for $struct_name {
fn on_container_open<'a>(
&'a self,
world: &'a Arc<$crate::world::World>,
_position: &'a pumpkin_util::math::position::BlockPos,
) -> $crate::block::viewer::ViewerFuture<'a, ()> {
Box::pin(async move {
self.play_sound(world, pumpkin_data::sound::Sound::BlockChestOpen)
.await;
})
}
fn on_container_close<'a>(
&'a self,
world: &'a Arc<$crate::world::World>,
_position: &'a pumpkin_util::math::position::BlockPos,
) -> $crate::block::viewer::ViewerFuture<'a, ()> {
Box::pin(async move {
self.play_sound(world, pumpkin_data::sound::Sound::BlockChestClose)
.await;
})
}
fn on_viewer_count_update<'a>(
&'a self,
world: &'a Arc<$crate::world::World>,
position: &'a pumpkin_util::math::position::BlockPos,
old: u16,
new: u16,
) -> $crate::block::viewer::ViewerFuture<'a, ()> {
Box::pin(async move {
// Trigger block animation
world
.add_synced_block_event(
*position,
Self::LID_ANIMATION_EVENT_TYPE,
new as u8,
)
.await;
// Update neighbors for redstone signal when viewer count changes
// This is controlled by the EMITS_REDSTONE constant on the struct
if Self::EMITS_REDSTONE && old != new {
// Update direct neighbors
world.clone().update_neighbors(position, None).await;
// Also update neighbors of the block below (strongly powered block)
// This ensures redstone components adjacent to the block below are notified
let below_pos = position.down();
world.clone().update_neighbors(&below_pos, None).await;
}
})
}
}
};
}
/// Implements helper methods for chest-like block entities.
///
/// Includes the `play_sound` method which handles sound positioning for single and double chests,
/// as well as `new()` and `get_viewer_count()` methods.
#[macro_export]
macro_rules! impl_chest_helper_methods {
($struct_name:ty) => {
impl $struct_name {
/// Returns the number of players currently viewing this chest
pub fn get_viewer_count(&self) -> u16 {
self.viewers.get_viewer_count()
}
#[must_use]
pub fn new(position: pumpkin_util::math::position::BlockPos) -> Self {
use std::array::from_fn;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::AtomicBool;
Self {
position,
items: tokio::sync::RwLock::new(from_fn(|_| ItemStack::EMPTY.clone())),
dirty: AtomicBool::new(false),
viewers: $crate::block::viewer::ViewerCountTracker::new(),
loot_table: StdMutex::new(None),
loot_table_seed: 0,
}
}
async fn play_sound(
&self,
world: &Arc<$crate::world::World>,
sound: pumpkin_data::sound::Sound,
) {
let mut rng = pumpkin_util::random::xoroshiro128::Xoroshiro::from_seed(
pumpkin_util::random::get_seed(),
);
let (block, state) = world.get_block_and_state(&self.position);
let properties = pumpkin_data::block_properties::ChestLikeProperties::from_state_id(
state.id, block,
);
let position = match properties.r#type {
pumpkin_data::block_properties::ChestType::Left => return,
pumpkin_data::block_properties::ChestType::Single => {
pumpkin_util::math::vector3::Vector3::new(
self.position.0.x as f64 + 0.5,
self.position.0.y as f64 + 0.5,
self.position.0.z as f64 + 0.5,
)
}
pumpkin_data::block_properties::ChestType::Right => {
let direction = pumpkin_data::HorizontalFacingExt::to_block_direction(
&properties.facing,
)
.to_offset();
pumpkin_util::math::vector3::Vector3::new(
self.position.0.x as f64 + 0.5 + direction.x as f64 * 0.5,
self.position.0.y as f64 + 0.5,
self.position.0.z as f64 + 0.5 + direction.z as f64 * 0.5,
)
}
};
world.play_sound_fine(
sound,
pumpkin_data::sound::SoundCategory::Blocks,
&position,
0.5,
pumpkin_util::random::RandomImpl::next_f32(&mut rng) * 0.1 + 0.9,
);
}
}
};
}