use std::{ pin::Pin, sync::{ Arc, atomic::{AtomicI32, Ordering}, }, }; use crossbeam::atomic::AtomicCell; use pumpkin_data::{entity::EntityType, world::WorldEvent}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::{ boundingbox::{BoundingBox, EntityDimensions}, position::BlockPos, vector3::Vector3, }; use crate::{block::entities::BlockEntity, world::World}; pub struct MobSpawnerBlockEntity { pub position: BlockPos, pub delay: AtomicI32, pub max_delay: i32, pub min_delay: i32, pub spawn_count: i32, pub spawn_range: i32, pub entity_type: AtomicCell>, } impl MobSpawnerBlockEntity { pub const ID: &'static str = "minecraft:mob_spawner"; pub const DEFAULT_DELAY: i32 = 20; pub const DEFAULT_MAX_SPAWN_DELAY: i32 = 800; pub const DEFAULT_MIN_SPAWN_DELAY: i32 = 200; pub const DEFAULT_SPAWN_COUNT: i32 = 4; pub const DEFAULT_SPAWN_RANGE: i32 = 4; #[must_use] pub const fn new(position: BlockPos, entity_type: Option<&'static EntityType>) -> Self { Self { position, delay: AtomicI32::new(Self::DEFAULT_DELAY), max_delay: Self::DEFAULT_MAX_SPAWN_DELAY, min_delay: Self::DEFAULT_MIN_SPAWN_DELAY, spawn_count: Self::DEFAULT_SPAWN_COUNT, spawn_range: Self::DEFAULT_SPAWN_RANGE, entity_type: AtomicCell::new(entity_type), } } pub fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) { // TODO: this is ugly af nbt.put_string("id", self.resource_location().to_string()); let position = self.get_position(); nbt.put_int("x", position.0.x); nbt.put_int("y", position.0.y); nbt.put_int("z", position.0.z); if let Some(entity_type) = self.entity_type.load() { let mut spawn_entry = NbtCompound::new(); let mut entity_nbt = NbtCompound::new(); entity_nbt.put_string("id", format!("minecraft:{}", entity_type.resource_name)); spawn_entry.put_compound("entity", entity_nbt); nbt.put_compound("SpawnData", spawn_entry); } } } impl MobSpawnerBlockEntity { async fn update_spawns(&self, world: &Arc) { let min_delay = self.min_delay; let max_delay = self.max_delay; self.delay.store( if max_delay <= min_delay { min_delay } else { min_delay + rand::random_range(0..max_delay - min_delay) }, Ordering::Relaxed, ); world.add_synced_block_event(self.position, 1, 0).await; } pub fn set_entity_type(&self, entity_type: &'static EntityType) { self.entity_type.store(Some(entity_type)); } } impl BlockEntity for MobSpawnerBlockEntity { fn resource_location(&self) -> &'static str { Self::ID } fn get_position(&self) -> BlockPos { self.position } fn tick<'a>(&'a self, world: &'a Arc) -> Pin + Send + 'a>> { Box::pin(async move { if let Some(entity_type) = &self.entity_type.load() { if self.delay.load(Ordering::Relaxed) == -1 { self.update_spawns(world).await; } else { self.delay.fetch_sub(1, Ordering::Relaxed); return; } let spawn_range = self.spawn_range; let mut update_spawns = false; for _ in 0..self.spawn_count { let pos = self.position.0; let spawn_pos = Vector3::new( pos.x as f64 + (rand::random::() + rand::random::()) * spawn_range as f64 + 0.5, (pos.y + rand::random_range(0..3) - 1) as f64, pos.z as f64 + (rand::random::() + rand::random::()) * spawn_range as f64 + 0.5, ); // TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes if !world.is_space_empty(BoundingBox::new_from_pos( spawn_pos.x, spawn_pos.y, spawn_pos.z, &EntityDimensions { width: entity_type.dimension[0], height: entity_type.dimension[1], eye_height: entity_type.eye_height, }, )) { continue; } let entity = crate::entity::r#type::from_type( entity_type, spawn_pos, world, uuid::Uuid::new_v4(), ); world.spawn_entity(entity).await; world.sync_world_event(WorldEvent::ParticlesMobblockSpawn, self.position, 0); update_spawns = true; } if update_spawns { self.update_spawns(world).await; } } }) } fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self where Self: Sized, { let delay = nbt.get_short("Delay").unwrap_or(Self::DEFAULT_DELAY as i16) as i32; let min_delay = nbt .get_int("MinSpawnDelay") .unwrap_or(Self::DEFAULT_MIN_SPAWN_DELAY); let max_delay = nbt .get_int("MaxSpawnDelay") .unwrap_or(Self::DEFAULT_MAX_SPAWN_DELAY); let spawn_count = nbt .get_int("SpawnCount") .unwrap_or(Self::DEFAULT_SPAWN_COUNT); let spawn_range = nbt .get_int("SpawnRange") .unwrap_or(Self::DEFAULT_SPAWN_RANGE); let entity_type = nbt .get_compound("SpawnData") .and_then(|data| data.get_compound("entity")) .and_then(|entity| entity.get_string("id")) .and_then(|id| { let name = id.strip_prefix("minecraft:").unwrap_or(id); EntityType::from_name(name) }); Self { position, delay: AtomicI32::new(delay), max_delay, min_delay, spawn_count, spawn_range, entity_type: AtomicCell::new(entity_type), } } fn write_nbt<'a>( &'a self, nbt: &'a mut NbtCompound, ) -> Pin + Send + 'a>> { Box::pin(async move { self.write_nbt(nbt); }) } fn chunk_data_nbt(&self) -> Option { let mut final_nbt = NbtCompound::new(); if let Some(entity_type) = self.entity_type.load() { let mut spawn_entry = NbtCompound::new(); let mut entity_nbt = NbtCompound::new(); entity_nbt.put_string("id", format!("minecraft:{}", entity_type.resource_name)); spawn_entry.put_compound("entity", entity_nbt); final_nbt.put_compound("SpawnData", spawn_entry); } Some(final_nbt) } fn as_any(&self) -> &dyn std::any::Any { self } }