use super::BlockEntity; use crate::entity::Entity; use crate::entity::item::ItemEntity; use crate::world::World; use pumpkin_data::Block; use pumpkin_data::block_properties::CampfireLikeProperties; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::recipes::{CookingRecipeKind, get_cooking_recipe_with_ingredient}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::chunk::io::Dirtiable; use rand::{RngExt, rng}; use std::sync::{Arc, Mutex}; pub struct CampfireBlockEntity { pub position: BlockPos, pub items: [Arc>; Self::SLOT_COUNT], pub cooking_times: [Mutex; Self::SLOT_COUNT], pub cooking_total_times: [Mutex; Self::SLOT_COUNT], } impl BlockEntity for CampfireBlockEntity { 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 mut entity = Self::new(position); if let Some(list) = nbt.get_list("Items") { for tag in list { if let Some(compound) = tag.extract_compound() { let slot = compound.get_byte("Slot").unwrap_or(0) as usize; if slot < Self::SLOT_COUNT && let Some(stack) = ItemStack::read_item_stack(compound) { entity.items[slot] = Arc::new(Mutex::new(stack)); } } } } if let Some(arr) = nbt.get_int_array("CookingTimes") { for (slot, &value) in arr.iter().enumerate().take(Self::SLOT_COUNT) { entity.cooking_times[slot] = Mutex::new(value); } } if let Some(arr) = nbt.get_int_array("CookingTotalTimes") { for (slot, &value) in arr.iter().enumerate().take(Self::SLOT_COUNT) { entity.cooking_total_times[slot] = Mutex::new(value); } } entity } fn write_nbt(&self, nbt: &mut NbtCompound) { self.write_cooking_nbt(nbt); } fn tick(&self, world: &Arc) { let block = world.get_block(&self.position); if block != &Block::CAMPFIRE && block != &Block::SOUL_CAMPFIRE { return; } let state_id = world.get_block_state(&self.position).id; let properties = CampfireLikeProperties::from_state_id(state_id); if !properties.lit { if self.cool_down() { self.mark_chunk_dirty(world); } return; } let mut changed = false; let mut completed = false; for slot in 0..Self::SLOT_COUNT { let stack = self.items[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); if stack.is_empty() { continue; } let Some(recipe) = get_cooking_recipe_with_ingredient(stack.item, CookingRecipeKind::CampfireCooking) else { continue; }; let total = { let mut total = self.cooking_total_times[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if *total <= 0 { let mut cooking_time = recipe.cookingtime; if let Some(server) = world.server.upgrade() { let mut event = crate::plugin::api::events::block::campfire_start::CampfireStartEvent::new( self.position, world.clone(), stack.clone(), slot as u8, cooking_time, ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { continue; } cooking_time = event.cooking_time; } *total = cooking_time; } *total }; let finished = { let mut progress = self.cooking_times[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *progress += 1; changed = true; *progress >= total }; if !finished { continue; } let Some(result_item) = Item::from_registry_key(recipe.result.id) else { continue; }; let mut result = ItemStack::new(recipe.result.count, result_item); if let Some(server) = world.server.upgrade() { let mut event = crate::plugin::api::events::block::block_cook::BlockCookEvent::new( self.position, world.clone(), stack.clone(), result.clone(), ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { continue; } result = event.result; } Self::spawn_cooked_item(world, self.position, result); *self.items[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = ItemStack::EMPTY.clone(); *self.cooking_times[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = 0; *self.cooking_total_times[slot] .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = 0; completed = true; } if completed { if let Some(block_entity) = world.get_block_entity(&self.position) { world.update_block_entity(&block_entity); } } else if changed { self.mark_chunk_dirty(world); } } fn chunk_data_nbt(&self) -> Option { let mut nbt = NbtCompound::new(); self.write_cooking_nbt(&mut nbt); Some(nbt) } fn on_block_replaced(self: Arc, world: &Arc, _position: &BlockPos) { for item in &self.items { let stack = std::mem::replace( &mut *item .lock() .unwrap_or_else(std::sync::PoisonError::into_inner), ItemStack::EMPTY.clone(), ); if !stack.is_empty() { world.scatter_stack( f64::from(self.position.0.x), f64::from(self.position.0.y), f64::from(self.position.0.z), stack, ); } } } fn as_any(&self) -> &dyn std::any::Any { self } } impl CampfireBlockEntity { pub const ID: &'static str = "minecraft:campfire"; pub const SLOT_COUNT: usize = 4; #[must_use] pub fn new(position: BlockPos) -> Self { Self { position, items: std::array::from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))), cooking_times: std::array::from_fn(|_| Mutex::new(0)), cooking_total_times: std::array::from_fn(|_| Mutex::new(0)), } } fn write_cooking_nbt(&self, nbt: &mut NbtCompound) { let mut items = Vec::new(); for (slot, item) in self.items.iter().enumerate() { let stack = item .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if !stack.is_empty() { let mut item_nbt = NbtCompound::new(); item_nbt.put_byte("Slot", slot as i8); stack.write_item_stack(&mut item_nbt); items.push(NbtTag::Compound(item_nbt)); } } nbt.put_list("Items", items); let cooking_times = self .cooking_times .iter() .map(|time| { *time .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) }) .collect(); nbt.put("CookingTimes", NbtTag::IntArray(cooking_times)); let cooking_total_times = self .cooking_total_times .iter() .map(|time| { *time .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) }) .collect(); nbt.put("CookingTotalTimes", NbtTag::IntArray(cooking_total_times)); } fn cool_down(&self) -> bool { let mut changed = false; for progress in &self.cooking_times { let mut progress = progress .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let cooled = (*progress - 2).max(0); if cooled != *progress { *progress = cooled; changed = true; } } changed } fn mark_chunk_dirty(&self, world: &Arc) { let chunk_position = self.position.chunk_position(); let _ = world.level.read_chunk_sync(&chunk_position, |chunk| { chunk.mark_dirty(true); }); } fn spawn_cooked_item(world: &Arc, position: BlockPos, stack: ItemStack) { let spawn_position = Vector3::new( f64::from(position.0.x) + 0.5 + rng().random_range(-0.35..0.35), f64::from(position.0.y) + 1.01, f64::from(position.0.z) + 0.5 + rng().random_range(-0.35..0.35), ); let entity = Entity::new(world.clone(), spawn_position, &EntityType::ITEM); world.spawn_entity(Arc::new(ItemEntity::new(entity, stack))); } }