Pumpkin/crates/pumpkin/src/block/entities/campfire.rs
2026-09-03 19:11:20 +02:00

299 lines
9.9 KiB
Rust

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<Mutex<ItemStack>>; Self::SLOT_COUNT],
pub cooking_times: [Mutex<i32>; Self::SLOT_COUNT],
pub cooking_total_times: [Mutex<i32>; 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<World>) {
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<NbtCompound> {
let mut nbt = NbtCompound::new();
self.write_cooking_nbt(&mut nbt);
Some(nbt)
}
fn on_block_replaced(self: Arc<Self>, world: &Arc<World>, _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<World>) {
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<World>, 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)));
}
}