feat: implement Mob Spawners

This commit is contained in:
Alexander Medvedev
2025-08-24 23:02:59 +02:00
parent 46c048483c
commit c066787cc1
23 changed files with 396 additions and 95 deletions

64
Cargo.lock generated
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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,

View File

@@ -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!()

View File

@@ -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::<Vec<_>>();
let variant_count = self.values.len() as u16;
let values_index = (0..self.values.len() as u16).collect::<Vec<_>>();
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

View File

@@ -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! {

View File

@@ -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;

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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"

View File

@@ -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<Option<&'static EntityType>>,
}
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<dyn SimpleWorld>) {
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<dyn SimpleWorld>) {
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::<f64>() + rand::random::<f64>()) * spawn_range as f64
+ 0.5,
(pos.y + rand::random_range(0..3) - 1) as f64,
pos.z as f64
+ (rand::random::<f64>() + rand::random::<f64>()) * 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<NbtCompound> {
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
}
}

View File

@@ -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<Self>) -> Option<Arc<dyn PropertyDelegate>> {
None
@@ -98,6 +101,9 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option<Arc<dyn BlockEntity>>
}
BarrelBlockEntity::ID => Arc::new(block_entity_from_generic::<BarrelBlockEntity>(nbt)),
HopperBlockEntity::ID => Arc::new(block_entity_from_generic::<HopperBlockEntity>(nbt)),
MobSpawnerBlockEntity::ID => {
Arc::new(block_entity_from_generic::<MobSpawnerBlockEntity>(nbt))
}
DropperBlockEntity::ID => Arc::new(block_entity_from_generic::<DropperBlockEntity>(nbt)),
ShulkerBoxBlockEntity::ID => {
Arc::new(block_entity_from_generic::<ShulkerBoxBlockEntity>(nbt))

View File

@@ -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)
}

View File

@@ -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<BlockDirection>,
);
async fn is_space_empty(&self, bounding_box: BoundingBox) -> bool;
async fn spawn_from_type(
self: Arc<Self>,
entity_type: &'static EntityType,
position: Vector3<f64>,
);
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<Arc<dyn BlockEntity>>;
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,

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<BlockRegistry> {
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<dyn FluidBehaviour>>,
}
#[async_trait]
impl BlockRegistryExt for BlockRegistry {
fn can_place_at(
&self,

View File

@@ -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<ItemRegistry> {
manager.register(EmptyBucketItem);
manager.register(FilledBucketItem);
manager.register(ShovelItem);
manager.register(SpawnEggItem);
manager.register(AxeItem);
manager.register(EndCrystalItem);
manager.register(MinecartItem);

View File

@@ -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::<MobSpawnerBlockEntity>()
{
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::<f32>() * 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
}
}

View File

@@ -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::<f32>() * 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;

View File

@@ -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<Self>,
entity_type: &'static EntityType,
position: Vector3<f64>,
) {
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;
}