From c066787cc1f6bc2307dadd31d91150f2ff4e5eeb Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 24 Aug 2025 23:02:59 +0200 Subject: [PATCH] feat: implement Mob Spawners --- Cargo.lock | 64 ++++-- pumpkin-data/Cargo.toml | 2 +- pumpkin-data/build/effect.rs | 8 +- pumpkin-data/build/enchantments.rs | 4 +- pumpkin-data/build/fluid.rs | 6 +- pumpkin-data/build/potion.rs | 4 +- pumpkin-data/build/potion_brewing.rs | 24 ++- pumpkin-data/build/spawn_egg.rs | 5 + pumpkin-protocol/Cargo.toml | 4 +- .../src/java/client/play/chunk_data.rs | 1 - pumpkin-registry/Cargo.toml | 2 +- pumpkin-world/Cargo.toml | 2 +- .../src/block/entities/mob_spawner.rs | 186 ++++++++++++++++++ pumpkin-world/src/block/entities/mod.rs | 6 + pumpkin-world/src/block/entities/piston.rs | 2 +- pumpkin-world/src/world.rs | 14 +- pumpkin/src/block/blocks/mod.rs | 1 + pumpkin/src/block/blocks/spawner.rs | 20 ++ pumpkin/src/block/registry.rs | 4 +- pumpkin/src/item/items/mod.rs | 3 + pumpkin/src/item/items/spawn_egg.rs | 71 +++++++ pumpkin/src/net/java/play.rs | 41 ---- pumpkin/src/world/mod.rs | 17 ++ 23 files changed, 396 insertions(+), 95 deletions(-) create mode 100644 pumpkin-world/src/block/entities/mob_spawner.rs create mode 100644 pumpkin/src/block/blocks/spawner.rs create mode 100644 pumpkin/src/item/items/spawn_egg.rs diff --git a/Cargo.lock b/Cargo.lock index d2dbf9e6d..953db2b2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,10 +125,12 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-compression" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +checksum = "6448dfb3960f0b038e88c781ead1e7eb7929dfc3a71a1336ec9086c00f6d1e75" dependencies = [ + "compression-codecs", + "compression-core", "flate2", "futures-core", "memchr", @@ -269,9 +271,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "block-buffer" @@ -422,6 +424,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "compression-codecs" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46cc6539bf1c592cff488b9f253b30bc0ec50d15407c2cf45e27bd8f308d5905" +dependencies = [ + "compression-core", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", +] + +[[package]] +name = "compression-core" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2957e823c15bde7ecf1e8b64e537aa03a6be5fda0e2334e99887669e75b12e01" + [[package]] name = "console-api" version = "0.8.1" @@ -496,7 +517,7 @@ dependencies = [ "cookie", "document-features", "idna", - "indexmap 2.10.0", + "indexmap 2.11.0", "log", "serde", "serde_derive", @@ -1075,7 +1096,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.11.0", "slab", "tokio", "tokio-util", @@ -1384,9 +1405,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -1825,9 +1846,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", "phf_shared", @@ -1836,9 +1857,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", "phf_shared", @@ -1846,9 +1867,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", "phf_shared", @@ -1859,9 +1880,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.12.1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -2168,7 +2189,7 @@ dependencies = [ name = "pumpkin-registry" version = "0.1.0-dev+1.21.8" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "pumpkin-protocol", "pumpkin-util", "serde", @@ -2734,9 +2755,12 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" @@ -3013,7 +3037,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_spanned", "toml_datetime", diff --git a/pumpkin-data/Cargo.toml b/pumpkin-data/Cargo.toml index cb324b95e..3a1ce027c 100644 --- a/pumpkin-data/Cargo.toml +++ b/pumpkin-data/Cargo.toml @@ -5,7 +5,7 @@ edition.workspace = true build = "build/build.rs" [dependencies] -phf = { version = "0.12.1", features = ["macros"] } +phf = { version = "0.13.1", features = ["macros"] } pumpkin-util = { path = "../pumpkin-util" } pumpkin-nbt = { path = "../pumpkin-nbt" } diff --git a/pumpkin-data/build/effect.rs b/pumpkin-data/build/effect.rs index 4de154b64..d54194ad0 100644 --- a/pumpkin-data/build/effect.rs +++ b/pumpkin-data/build/effect.rs @@ -69,16 +69,16 @@ pub(crate) fn build() -> TokenStream { let mut name_to_type = TokenStream::new(); let mut minecraft_name_to_type = TokenStream::new(); - for (name, effect) in effects.iter() { + for (name, effect) in effects.into_iter() { let format_name = format_ident!("{}", name.to_shouty_snake_case()); let id = effect.id; let color = effect.color; - let translation_key = effect.translation_key.clone(); + let translation_key = effect.translation_key; let category = effect.category.to_tokens(); - let slots = effect.attribute_modifiers.clone(); + let slots = effect.attribute_modifiers; let slots = slots.iter().map(|slot| slot.to_tokens()); - let minecraft_name = "minecraft:".to_string() + name; + let minecraft_name = "minecraft:".to_string() + &name; variants.extend([quote! { pub const #format_name: Self = Self { minecraft_name: #minecraft_name, diff --git a/pumpkin-data/build/enchantments.rs b/pumpkin-data/build/enchantments.rs index 81923f6ea..d6a0b2a8c 100644 --- a/pumpkin-data/build/enchantments.rs +++ b/pumpkin-data/build/enchantments.rs @@ -63,7 +63,7 @@ pub(crate) fn build() -> TokenStream { let mut name_to_type = TokenStream::new(); let mut id_to_type = TokenStream::new(); - for (name, enchantment) in enchantments.iter() { + for (name, enchantment) in enchantments.into_iter() { let id = enchantment.id; let raw_name = name.strip_prefix("minecraft:").unwrap(); let format_name = format_ident!("{}", raw_name.to_shouty_snake_case()); @@ -79,7 +79,7 @@ pub(crate) fn build() -> TokenStream { .to_uppercase() ); let max_level = enchantment.max_level; - let slots = enchantment.slots.clone(); + let slots = enchantment.slots; let slots = slots.iter().map(|slot| slot.to_tokens()); let Translate { translate, with: _ } = &enchantment.description.0.content else { panic!() diff --git a/pumpkin-data/build/fluid.rs b/pumpkin-data/build/fluid.rs index d95c8aed7..b47696240 100644 --- a/pumpkin-data/build/fluid.rs +++ b/pumpkin-data/build/fluid.rs @@ -53,8 +53,8 @@ impl ToTokens for PropertyStruct { fn to_tokens(&self, tokens: &mut TokenStream) { let name = Ident::new(&self.name, Span::call_site()); - let variant_count = self.values.clone().len() as u16; - let values_index = (0..self.values.clone().len() as u16).collect::>(); + let variant_count = self.values.len() as u16; + let values_index = (0..self.values.len() as u16).collect::>(); let ident_values = self.values.iter().map(|value| { let value_str = if value.chars().all(|c| c.is_numeric()) { @@ -370,7 +370,7 @@ pub(crate) fn build() -> TokenStream { let mut unique_states = Vec::new(); let mut optimized_fluids: Vec<(String, FluidStateRef)> = Vec::new(); - for fluid in fluids.clone() { + for fluid in fluids { let id_name = LitStr::new(&fluid.name, proc_macro2::Span::call_site()); let const_ident = format_ident!("{}", fluid.name.to_shouty_snake_case()); let state_id_start = fluid diff --git a/pumpkin-data/build/potion.rs b/pumpkin-data/build/potion.rs index 775df6599..1e783edbb 100644 --- a/pumpkin-data/build/potion.rs +++ b/pumpkin-data/build/potion.rs @@ -59,10 +59,10 @@ pub(crate) fn build() -> TokenStream { let mut variants = TokenStream::new(); let mut name_to_type = TokenStream::new(); - for (name, potion) in potions.iter() { + for (name, potion) in potions.into_iter() { let format_name = format_ident!("{}", name.to_shouty_snake_case()); let id = potion.id; - let slots = potion.effects.clone(); + let slots = potion.effects; let slots = slots.iter().map(|slot| slot.to_tokens()); variants.extend([quote! { diff --git a/pumpkin-data/build/potion_brewing.rs b/pumpkin-data/build/potion_brewing.rs index e7c1d0e0d..58095c345 100644 --- a/pumpkin-data/build/potion_brewing.rs +++ b/pumpkin-data/build/potion_brewing.rs @@ -18,7 +18,7 @@ pub struct Recipes { } impl Recipes { - pub fn to_tokens_potion(&self) -> TokenStream { + pub fn get_tokens_potion(self) -> TokenStream { let from = format_ident!( "{}", self.from.strip_prefix("minecraft:").unwrap().to_uppercase() @@ -28,8 +28,7 @@ impl Recipes { self.to.strip_prefix("minecraft:").unwrap().to_uppercase() ); - let slots = self.ingredient.clone(); - let slots = slots.iter().map(|slot| { + let slots = self.ingredient.iter().map(|slot| { format_ident!( "{}", slot.strip_prefix("minecraft:").unwrap().to_uppercase() @@ -45,7 +44,7 @@ impl Recipes { } } - pub fn to_tokens_item(&self) -> TokenStream { + pub fn get_tokens_item(self) -> TokenStream { let from = format_ident!( "{}", self.from.strip_prefix("minecraft:").unwrap().to_uppercase() @@ -55,8 +54,7 @@ impl Recipes { self.to.strip_prefix("minecraft:").unwrap().to_uppercase() ); - let slots = self.ingredient.clone(); - let slots = slots.iter().map(|slot| { + let slots = self.ingredient.iter().map(|slot| { format_ident!( "{}", slot.strip_prefix("minecraft:").unwrap().to_uppercase() @@ -83,16 +81,16 @@ pub(crate) fn build() -> TokenStream { let mut item = TokenStream::new(); let mut potion = TokenStream::new(); - for j in &json.item_recipes { - item.extend(j.to_tokens_item()); - } - for j in &json.potion_recipes { - potion.extend(j.to_tokens_potion()); - } - let item_len = json.item_recipes.len(); let potion_len = json.potion_recipes.len(); + for j in json.item_recipes { + item.extend(j.get_tokens_item()); + } + for j in json.potion_recipes { + potion.extend(j.get_tokens_potion()); + } + quote! { #![allow(dead_code)] use crate::potion::Potion; diff --git a/pumpkin-data/build/spawn_egg.rs b/pumpkin-data/build/spawn_egg.rs index c7b18a495..95b73cf99 100644 --- a/pumpkin-data/build/spawn_egg.rs +++ b/pumpkin-data/build/spawn_egg.rs @@ -11,10 +11,12 @@ pub(crate) fn build() -> TokenStream { serde_json::from_str(&fs::read_to_string("../assets/spawn_egg.json").unwrap()) .expect("Failed to parse spawn_egg.json"); let mut names = TokenStream::new(); + let mut ids = TokenStream::new(); for (egg, entity) in &eggs { let entity = entity.to_shouty_snake_case(); let entity = format_ident!("{}", entity); + ids.extend(quote! { #egg, }); names.extend(quote! { #egg => Some(&EntityType::#entity), }); } quote! { @@ -26,5 +28,8 @@ pub(crate) fn build() -> TokenStream { _ => None } } + pub fn spawn_egg_ids() -> Box<[u16]> { + [#ids].into() + } } } diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index c0fd75a28..e86ab4bf9 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -27,10 +27,10 @@ aes = "0.8" cfb8 = "0.8" # compression -async-compression = { version = "0.4.27", features = ["tokio", "zlib"] } +async-compression = { version = "0.4.28", features = ["tokio", "zlib"] } take_mut = "0.2.2" -bitflags = "2.9.2" +bitflags = "2.9.3" [dev-dependencies] # Validate correctness diff --git a/pumpkin-protocol/src/java/client/play/chunk_data.rs b/pumpkin-protocol/src/java/client/play/chunk_data.rs index dc940fc63..d25200b9f 100644 --- a/pumpkin-protocol/src/java/client/play/chunk_data.rs +++ b/pumpkin-protocol/src/java/client/play/chunk_data.rs @@ -118,7 +118,6 @@ impl ClientPacket for CChunkData<'_> { write.write_slice(&blocks_and_biomes_buf)?; } - // TODO: block entities write.write_var_int(&VarInt(self.0.block_entities.len() as i32))?; for block_entity in self.0.block_entities.values() { let block_entity = &block_entity; diff --git a/pumpkin-registry/Cargo.toml b/pumpkin-registry/Cargo.toml index ae997e238..d721837d1 100644 --- a/pumpkin-registry/Cargo.toml +++ b/pumpkin-registry/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true pumpkin-protocol = { path = "../pumpkin-protocol" } pumpkin-util = { path = "../pumpkin-util" } -indexmap = { version = "2.10", features = ["serde"] } +indexmap = { version = "2.11", features = ["serde"] } serde.workspace = true serde_json.workspace = true diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index 9c6562efa..d135a2961 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -26,7 +26,7 @@ crossbeam.workspace = true num-derive = "0.4" sha2 = "0.10" -bitflags = "2.9.2" +bitflags = "2.9.3" dashmap = "6.1" diff --git a/pumpkin-world/src/block/entities/mob_spawner.rs b/pumpkin-world/src/block/entities/mob_spawner.rs new file mode 100644 index 000000000..5ab21079a --- /dev/null +++ b/pumpkin-world/src/block/entities/mob_spawner.rs @@ -0,0 +1,186 @@ +use std::sync::{ + Arc, + atomic::{AtomicI32, Ordering}, +}; + +use async_trait::async_trait; +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::SimpleWorld}; + +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; + + pub fn new(position: BlockPos) -> 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(None), + } + } +} + +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)); + } +} + +#[async_trait] +impl BlockEntity for MobSpawnerBlockEntity { + fn resource_location(&self) -> &'static str { + Self::ID + } + + fn get_position(&self) -> BlockPos { + self.position + } + + async fn tick(&self, world: Arc) { + 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], + }, + )) + .await + { + continue; + } + world.clone().spawn_from_type(entity_type, spawn_pos).await; + world + .sync_world_event(WorldEvent::SpawnerSpawnsMob, self.position, 0) + .await; + 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 _spawn_entry = nbt.get_compound("SpawnData"); + + Self { + position, + delay: AtomicI32::new(delay), + max_delay, + min_delay, + spawn_count, + spawn_range, + entity_type: AtomicCell::new(None), // TODO + } + } + + async fn write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + if let Some(entity_type) = self.entity_type.load() { + let mut entity_nbt = NbtCompound::new(); + entity_nbt.put_string("id", format!("minecraft:{}", entity_type.resource_name)); + + nbt.put_component("entity", entity_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_component("entity", entity_nbt); + + final_nbt.put_component("SpawnData", spawn_entry); + } + Some(final_nbt) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin-world/src/block/entities/mod.rs b/pumpkin-world/src/block/entities/mod.rs index e0b2c450b..a738f48f9 100644 --- a/pumpkin-world/src/block/entities/mod.rs +++ b/pumpkin-world/src/block/entities/mod.rs @@ -14,6 +14,7 @@ use pumpkin_util::math::position::BlockPos; use sign::SignBlockEntity; use crate::block::entities::hopper::HopperBlockEntity; +use crate::block::entities::mob_spawner::MobSpawnerBlockEntity; use crate::block::entities::shulker_box::ShulkerBoxBlockEntity; use crate::{ BlockStateId, block::entities::chiseled_bookshelf::ChiseledBookshelfBlockEntity, @@ -30,6 +31,7 @@ pub mod dropper; pub mod end_portal; pub mod furnace; pub mod hopper; +pub mod mob_spawner; pub mod piston; pub mod shulker_box; pub mod sign; @@ -75,6 +77,7 @@ pub trait BlockEntity: Send + Sync { fn is_dirty(&self) -> bool { false } + fn as_any(&self) -> &dyn Any; fn to_property_delegate(self: Arc) -> Option> { None @@ -98,6 +101,9 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option> } BarrelBlockEntity::ID => Arc::new(block_entity_from_generic::(nbt)), HopperBlockEntity::ID => Arc::new(block_entity_from_generic::(nbt)), + MobSpawnerBlockEntity::ID => { + Arc::new(block_entity_from_generic::(nbt)) + } DropperBlockEntity::ID => Arc::new(block_entity_from_generic::(nbt)), ShulkerBoxBlockEntity::ID => { Arc::new(block_entity_from_generic::(nbt)) diff --git a/pumpkin-world/src/block/entities/piston.rs b/pumpkin-world/src/block/entities/piston.rs index c3494b261..7c0b4518b 100644 --- a/pumpkin-world/src/block/entities/piston.rs +++ b/pumpkin-world/src/block/entities/piston.rs @@ -134,7 +134,7 @@ impl BlockEntity for PistonBlockEntity { nbt.put_float(LAST_PROGRESS, self.last_progress.load()); nbt.put_bool(EXTENDING, self.extending); nbt.put_bool(SOURCE, self.source); - // TODO: duplicated code :c + // TODO: duplicated code because of async :c Some(nbt) } diff --git a/pumpkin-world/src/world.rs b/pumpkin-world/src/world.rs index fc602c18c..2cb645a19 100644 --- a/pumpkin-world/src/world.rs +++ b/pumpkin-world/src/world.rs @@ -4,8 +4,11 @@ use crate::block::entities::BlockEntity; use crate::{BlockStateId, inventory::Inventory}; use async_trait::async_trait; use bitflags::bitflags; +use pumpkin_data::entity::EntityType; use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::world::WorldEvent; use pumpkin_data::{Block, BlockDirection, BlockState}; +use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use thiserror::Error; @@ -58,8 +61,18 @@ pub trait SimpleWorld: BlockAccessor + Send + Sync { except: Option, ); + async fn is_space_empty(&self, bounding_box: BoundingBox) -> bool; + + async fn spawn_from_type( + self: Arc, + entity_type: &'static EntityType, + position: Vector3, + ); + async fn add_synced_block_event(&self, pos: BlockPos, r#type: u8, data: u8); + async fn sync_world_event(&self, world_event: WorldEvent, position: BlockPos, data: i32); + async fn remove_block_entity(&self, block_pos: &BlockPos); async fn get_block_entity(&self, block_pos: &BlockPos) -> Option>; async fn get_world_age(&self) -> i64; @@ -82,7 +95,6 @@ pub trait SimpleWorld: BlockAccessor + Send + Sync { ); } -#[async_trait] pub trait BlockRegistryExt: Send + Sync { fn can_place_at( &self, diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 9e6a59d4a..e51a24668 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -44,6 +44,7 @@ pub mod redstone; pub mod signs; pub mod skull_block; pub mod slabs; +pub mod spawner; pub mod stairs; pub mod sugar_cane; pub mod tnt; diff --git a/pumpkin/src/block/blocks/spawner.rs b/pumpkin/src/block/blocks/spawner.rs new file mode 100644 index 000000000..42900bbdd --- /dev/null +++ b/pumpkin/src/block/blocks/spawner.rs @@ -0,0 +1,20 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use pumpkin_macros::pumpkin_block; +use pumpkin_world::block::entities::mob_spawner::MobSpawnerBlockEntity; + +use crate::block::{BlockBehaviour, PlacedArgs}; + +#[pumpkin_block("minecraft:spawner")] +pub struct SpawnerBlock; + +#[async_trait] +impl BlockBehaviour for SpawnerBlock { + async fn placed(&self, args: PlacedArgs<'_>) { + let hopper_block_entity = MobSpawnerBlockEntity::new(*args.position); + args.world + .add_block_entity(Arc::new(hopper_block_entity)) + .await; + } +} diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index 5820bb43c..03ffa91eb 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -60,6 +60,7 @@ use crate::block::blocks::redstone::tripwire::TripwireBlock; use crate::block::blocks::redstone::tripwire_hook::TripwireHookBlock; use crate::block::blocks::signs::SignBlock; use crate::block::blocks::slabs::SlabBlock; +use crate::block::blocks::spawner::SpawnerBlock; use crate::block::blocks::stairs::StairBlock; use crate::block::blocks::sugar_cane::SugarCaneBlock; use crate::block::blocks::tnt::TNTBlock; @@ -74,7 +75,6 @@ use crate::entity::EntityBase; use crate::entity::player::Player; use crate::server::Server; use crate::world::World; -use async_trait::async_trait; use pumpkin_data::fluid; use pumpkin_data::fluid::Fluid; use pumpkin_data::item::Item; @@ -201,6 +201,7 @@ pub fn default_registry() -> Arc { manager.register(PressurePlateBlock); manager.register(WeightedPressurePlateBlock); manager.register(EndPortalBlock); + manager.register(SpawnerBlock); manager.register(EndPortalFrameBlock); manager.register(CandleBlock); manager.register(SeaPickleBlock); @@ -290,7 +291,6 @@ pub struct BlockRegistry { fluids: HashMap<&'static Fluid, Arc>, } -#[async_trait] impl BlockRegistryExt for BlockRegistry { fn can_place_at( &self, diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index 8a55b96fd..87865103f 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -14,12 +14,14 @@ pub mod minecart; pub mod name_tag; pub mod shovel; pub mod snowball; +pub mod spawn_egg; pub mod swords; pub mod trident; use crate::item::items::end_crystal::EndCrystalItem; use crate::item::items::minecart::MinecartItem; use crate::item::items::name_tag::NameTagItem; +use crate::item::items::spawn_egg::SpawnEggItem; use super::registry::ItemRegistry; use axe::AxeItem; @@ -54,6 +56,7 @@ pub fn default_registry() -> Arc { manager.register(EmptyBucketItem); manager.register(FilledBucketItem); manager.register(ShovelItem); + manager.register(SpawnEggItem); manager.register(AxeItem); manager.register(EndCrystalItem); manager.register(MinecartItem); diff --git a/pumpkin/src/item/items/spawn_egg.rs b/pumpkin/src/item/items/spawn_egg.rs new file mode 100644 index 000000000..d4535366d --- /dev/null +++ b/pumpkin/src/item/items/spawn_egg.rs @@ -0,0 +1,71 @@ +use crate::entity::player::Player; +use crate::entity::r#type::from_type; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use async_trait::async_trait; +use pumpkin_data::entity::entity_from_egg; +use pumpkin_data::{Block, BlockDirection}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::wrap_degrees; +use pumpkin_world::block::entities::mob_spawner::MobSpawnerBlockEntity; +use pumpkin_world::item::ItemStack; +use uuid::Uuid; + +pub struct SpawnEggItem; + +impl ItemMetadata for SpawnEggItem { + fn ids() -> Box<[u16]> { + pumpkin_data::entity::spawn_egg_ids() + } +} + +#[async_trait] +impl ItemBehaviour for SpawnEggItem { + async fn use_on_block( + &self, + item: &mut ItemStack, + player: &Player, + location: BlockPos, + face: BlockDirection, + _block: &Block, + _server: &Server, + ) { + if let Some(entity_type) = entity_from_egg(item.item.id) { + let world = player.world(); + + if let Some(block_entity) = player.world().get_block_entity(&location).await + && let Some(spawner) = block_entity + .as_any() + .downcast_ref::() + { + spawner.set_entity_type(entity_type); + world.update_block_entity(&block_entity).await; + item.decrement_unless_creative(player.gamemode.load(), 1); + return; + } + let pos = BlockPos(location.0 + face.to_offset()); + let pos = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y), + f64::from(pos.0.z) + 0.5, + ); + // Create rotation like Vanilla + let yaw = wrap_degrees(rand::random::() * 360.0) % 360.0; + + let mob = from_type(entity_type, pos, world, Uuid::new_v4()).await; + + // Set the rotation + mob.get_entity().set_rotation(yaw, 0.0); + + // Broadcast the new mob to all players + world.spawn_entity(mob).await; + item.decrement_unless_creative(player.gamemode.load(), 1); + // TODO: send/configure additional commands/data based on the type of entity (horse, slime, etc) + } + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index e4c71d403..93dd7da37 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -15,7 +15,6 @@ use crate::block::{self, BlockIsReplacing}; use crate::command::CommandSender; use crate::entity::EntityBase; use crate::entity::player::{ChatMode, ChatSession, Player}; -use crate::entity::r#type::from_type; use crate::error::PumpkinError; use crate::net::PlayerConfig; use crate::net::java::JavaClient; @@ -28,7 +27,6 @@ use crate::world::{World, chunker}; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::block_properties::{BlockProperties, WaterLikeProperties}; use pumpkin_data::data_component_impl::{ConsumableImpl, EquipmentSlot, FoodImpl}; -use pumpkin_data::entity::{EntityType, entity_from_egg}; use pumpkin_data::item::Item; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::{Block, BlockDirection, BlockState}; @@ -60,7 +58,6 @@ use pumpkin_world::block::entities::sign::SignBlockEntity; use pumpkin_world::item::ItemStack; use pumpkin_world::world::BlockFlags; use tokio::sync::Mutex; -use uuid::Uuid; /// In secure chat mode, Player will be kicked if they send a chat message with a timestamp that is older than this (in ms) /// Vanilla: 2 minutes @@ -1460,14 +1457,6 @@ impl JavaClient { .await?; } - // Check if the item is a spawn egg - let item_id = stack.item.id; - if let Some(entity) = entity_from_egg(item_id) { - self.spawn_entity_from_egg(player, entity, position, face) - .await; - should_try_decrement = true; - } - if should_try_decrement { // TODO: Config // Decrease block count @@ -1751,36 +1740,6 @@ impl JavaClient { ); } - async fn spawn_entity_from_egg( - &self, - player: &Player, - entity_type: &'static EntityType, - location: BlockPos, - face: BlockDirection, - ) { - let world_pos = BlockPos(location.0 + face.to_offset()); - // Align the position like Vanilla does - let pos = Vector3::new( - f64::from(world_pos.0.x) + 0.5, - f64::from(world_pos.0.y), - f64::from(world_pos.0.z) + 0.5, - ); - // Create rotation like Vanilla - let yaw = wrap_degrees(rand::random::() * 360.0) % 360.0; - - let world = player.world(); - // Create a new mob and UUID based on the spawn egg id - let mob = from_type(entity_type, pos, world, Uuid::new_v4()).await; - - // Set the rotation - mob.get_entity().set_rotation(yaw, 0.0); - - // Broadcast the new mob to all players - world.spawn_entity(mob).await; - - // TODO: send/configure additional commands/data based on the type of entity (horse, slime, etc) - } - const WORLD_LOWEST_Y: i8 = -64; const WORLD_MAX_Y: u16 = 320; diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index b1ff85e81..c1cb3e8fd 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -3202,10 +3202,27 @@ impl pumpkin_world::world::SimpleWorld for World { Self::update_neighbors(&self, block_pos, except).await; } + async fn is_space_empty(&self, bounding_box: BoundingBox) -> bool { + self.is_space_empty(bounding_box).await + } + async fn add_synced_block_event(&self, pos: BlockPos, r#type: u8, data: u8) { self.add_synced_block_event(pos, r#type, data).await; } + async fn sync_world_event(&self, world_event: WorldEvent, position: BlockPos, data: i32) { + self.sync_world_event(world_event, position, data).await; + } + + async fn spawn_from_type( + self: Arc, + entity_type: &'static EntityType, + position: Vector3, + ) { + let mob = from_type(entity_type, position, &self, Uuid::new_v4()).await; + self.spawn_entity(mob).await; + } + async fn remove_block_entity(&self, block_pos: &BlockPos) { self.remove_block_entity(block_pos).await; }