From 0dfaf5d213e9bc281defa8315945160eddbc7f47 Mon Sep 17 00:00:00 2001 From: BouncingElf10 <168445249+BouncingElf10@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:36:10 +0200 Subject: [PATCH] feat(block): Added more dispenser behaviors (#2495) * feat(block): added DispenseContext and drop_item abstractions for dispenser behavior * feat(block): added specific arrow dispenser behavior * feat(block): added specific spawn dispenser behavior * docs(block): added some comments to quickly spot missing dispenser behaviors * fix: addressed PR review feedback --- .../src/block/blocks/redstone/dispenser.rs | 301 ++++++++++++++++-- pumpkin/src/item/items/boat.rs | 2 +- pumpkin/src/item/items/spawn_egg.rs | 2 +- 3 files changed, 268 insertions(+), 37 deletions(-) diff --git a/pumpkin/src/block/blocks/redstone/dispenser.rs b/pumpkin/src/block/blocks/redstone/dispenser.rs index aeead2cb9..05d172582 100644 --- a/pumpkin/src/block/blocks/redstone/dispenser.rs +++ b/pumpkin/src/block/blocks/redstone/dispenser.rs @@ -1,6 +1,7 @@ use rand::{Rng, RngExt, rng}; use std::sync::Arc; use tokio::sync::Mutex; +use uuid::Uuid; use crate::block::blocks::redstone::block_receives_redstone_power; use crate::block::registry::BlockActionResult; @@ -8,13 +9,26 @@ use crate::block::{ BlockBehaviour, BlockFuture, GetComparatorOutputArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, PlacedArgs, }; +use crate::entity::decoration::armor_stand::ArmorStandEntity; use crate::entity::item::ItemEntity; +use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup}; +use crate::entity::tnt::TNTEntity; +use crate::entity::r#type::from_type; +use crate::entity::vehicle::boat::BoatEntity; use crate::entity::{Entity, EntityBase}; +use crate::item::ItemMetadata; +use crate::item::items::boat::BoatItem; +use crate::item::items::spawn_egg::apply_entity_variant; +use crate::world::World; use crate::block::entities::dispenser::DispenserBlockEntity; use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::{BlockProperties, Facing}; -use pumpkin_data::entity::EntityType; +use pumpkin_data::entity::{EntityType, entity_from_egg}; +use pumpkin_data::fluid::Fluid; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::translation; use pumpkin_data::world::WorldEvent; use pumpkin_inventory::generic_container_screen_handler::create_generic_3x3; @@ -23,7 +37,10 @@ use pumpkin_inventory::screen_handler::{ BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, }; use pumpkin_macros::pumpkin_block; +use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions}; +use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::wrap_degrees; use pumpkin_util::text::TextComponent; use pumpkin_world::inventory::Inventory; use pumpkin_world::tick::TickPriority; @@ -60,6 +77,22 @@ pub struct DispenserBlock; type DispenserLikeProperties = pumpkin_data::block_properties::DispenserLikeProperties; +struct DispenseContext<'a> { + world: &'a Arc, + position: &'a BlockPos, + facing: Facing, +} + +impl<'a> DispenseContext<'a> { + const fn new(args: &OnScheduledTickArgs<'a>, facing: Facing) -> Self { + Self { + world: args.world, + position: args.position, + facing, + } + } +} + fn triangle(rng: &mut R, min: f64, max: f64) -> f64 { (rng.random::() - rng.random::()).mul_add(max, min) } @@ -163,42 +196,40 @@ impl BlockBehaviour for DispenserBlock { args.world.get_block_state(args.position).id, args.block, ); + let ctx = DispenseContext::new(&args, props.facing); - // No specific dispenser behaviors are registered yet, so dispense item into the world - let drop_item = item.split(1); - let facing = to_normal(props.facing); - let mut position = args.position.to_centered_f64().add(&(facing * 0.7)); + // Still missing some specific dispenser behavior that you can find here: + // https://minecraft.wiki/w/Dispenser#Usage + let arrows = [ + Item::ARROW.id, + Item::TIPPED_ARROW.id, + Item::SPECTRAL_ARROW.id, + ]; + let boats = BoatItem::ids(); - position.y -= match props.facing { - Facing::Up | Facing::Down => 0.125, - _ => 0.15625, - }; - - let entity = Entity::new(args.world.clone(), position, &EntityType::ITEM); - let rd = rng().random::().mul_add(0.1, 0.2); - - let velocity = Vector3::new( - triangle(&mut rng(), facing.x * rd, 0.017_227_5 * 6.), - triangle(&mut rng(), 0.2, 0.017_227_5 * 6.), - triangle(&mut rng(), facing.z * rd, 0.017_227_5 * 6.), - ); - - let item_entity = Arc::new(ItemEntity::new_with_velocity( - entity, drop_item, velocity, 40, - )); - args.world.spawn_entity(item_entity).await; - - args.world.sync_world_event( - WorldEvent::SoundDispenserDispense, - *args.position, - 0, - ); - - args.world.sync_world_event( - WorldEvent::ParticlesShootSmoke, - *args.position, - to_data3d(props.facing), - ); + if arrows.contains(&item.item.id) { + // Arrows + Self::fire_arrow(&ctx, &mut item).await; + } else if boats.contains(&item.item.id) { + // Boats + if !Self::dispense_boat(&ctx, &mut item).await { + Self::drop_item(&ctx, &mut item).await; + } + } else if item.item.id == Item::ARMOR_STAND.id { + // Armor stands + if !Self::dispense_armor_stand(&ctx, &mut item).await { + Self::drop_item(&ctx, &mut item).await; + } + } else if item.item.id == Item::TNT.id { + // TNT + Self::dispense_tnt(&ctx, &mut item).await; + } else if entity_from_egg(item.item.id).is_some() { + // Spawn eggs + Self::dispense_spawn_egg(&ctx, &mut item).await; + } else { + // Default / Drop + Self::drop_item(&ctx, &mut item).await; + } } else { args.world .sync_world_event(WorldEvent::SoundDispenserFail, *args.position, 0); @@ -222,3 +253,203 @@ impl BlockBehaviour for DispenserBlock { }) } } + +impl DispenserBlock { + const ARROW_DISPENSE_POWER: f64 = 1.1; + const ARROW_DISPENSE_UNCERTAINTY: f64 = 6.0; + + async fn fire_arrow(ctx: &DispenseContext<'_>, item: &mut ItemStack) { + // TODO: Add tipped arrows + let entity_type = if item.item.id == Item::SPECTRAL_ARROW.id { + &EntityType::SPECTRAL_ARROW + } else { + &EntityType::ARROW + }; + let _ = item.split(1); + + let facing = to_normal(ctx.facing); + let position = ctx.position.to_centered_f64().add(&(facing * 0.7)); + let world = ctx.world; + + let arrow_entity = Entity::new(world.clone(), position, entity_type); + let mut arrow = ArrowEntity::new(arrow_entity, None); + arrow.pickup = ArrowPickup::Allowed; + + arrow.set_velocity( + facing.x, + facing.y + 0.1, + facing.z, + Self::ARROW_DISPENSE_POWER, + Self::ARROW_DISPENSE_UNCERTAINTY, + ); + + let arrow_arc: Arc = Arc::new(arrow); + world.spawn_entity(arrow_arc).await; + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserProjectileLaunch, *ctx.position, 0); + + ctx.world.sync_world_event( + WorldEvent::ParticlesShootSmoke, + *ctx.position, + to_data3d(ctx.facing), + ); + } + + fn target_position(ctx: &DispenseContext<'_>) -> BlockPos { + let facing = to_normal(ctx.facing); + ctx.position.offset(Vector3::new( + facing.x as i32, + facing.y as i32, + facing.z as i32, + )) + } + + fn has_room_for( + ctx: &DispenseContext<'_>, + spawn_pos: Vector3, + size: &EntityDimensions, + ) -> bool { + let bounding_box = BoundingBox::new_from_pos(spawn_pos.x, spawn_pos.y, spawn_pos.z, size); + ctx.world.is_space_empty(bounding_box) + && ctx.world.get_entities_at_box(&bounding_box).is_empty() + } + + async fn dispense_boat(ctx: &DispenseContext<'_>, item: &mut ItemStack) -> bool { + let target = Self::target_position(ctx); + let is_water = |id: u16| id == Fluid::WATER.id || id == Fluid::FLOWING_WATER.id; + + let spawn_pos = if is_water(ctx.world.get_fluid(&target).id) { + target.to_f64() + } else if ctx.world.get_block_state(&target).is_air() + && is_water(ctx.world.get_fluid(&target.down()).id) + { + target.down().to_f64() + } else { + return false; + }; + + let entity_type = BoatItem::item_to_entity(item.item); + let dimensions = EntityDimensions::new( + entity_type.dimension[0], + entity_type.dimension[1], + entity_type.eye_height, + ); + if !Self::has_room_for(ctx, spawn_pos, &dimensions) { + return false; + } + + let _ = item.split(1); + let facing = to_normal(ctx.facing); + let entity = Entity::new(ctx.world.clone(), spawn_pos, entity_type); + entity.set_rotation(facing.x.atan2(facing.z) as f32 * 57.295_776, 0.0); + ctx.world + .spawn_entity(Arc::new(BoatEntity::new(entity))) + .await; + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0); + true + } + + async fn dispense_armor_stand(ctx: &DispenseContext<'_>, item: &mut ItemStack) -> bool { + let target = Self::target_position(ctx); + let spawn_pos = target.to_f64(); + let dimensions = EntityDimensions::new( + EntityType::ARMOR_STAND.dimension[0], + EntityType::ARMOR_STAND.dimension[1], + EntityType::ARMOR_STAND.eye_height, + ); + if !Self::has_room_for(ctx, spawn_pos, &dimensions) { + return false; + } + + let _ = item.split(1); + let facing = to_normal(ctx.facing); + let entity = Entity::new(ctx.world.clone(), spawn_pos, &EntityType::ARMOR_STAND); + entity.set_rotation(facing.x.atan2(facing.z) as f32 * 57.295_776, 0.0); + + ctx.world.play_sound( + Sound::EntityArmorStandPlace, + SoundCategory::Blocks, + &spawn_pos, + ); + ctx.world + .spawn_entity(Arc::new(ArmorStandEntity::new(entity))) + .await; + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0); + true + } + + async fn dispense_tnt(ctx: &DispenseContext<'_>, item: &mut ItemStack) { + const TNT_POWER: f32 = 4.0; + const TNT_FUSE: u32 = 80; + + let _ = item.split(1); + let spawn_pos = Self::target_position(ctx).to_f64(); + + let entity = Entity::new(ctx.world.clone(), spawn_pos, &EntityType::TNT); + let tnt = Arc::new(TNTEntity::new(entity, TNT_POWER, TNT_FUSE)); + ctx.world.spawn_entity(tnt).await; + ctx.world + .play_sound(Sound::EntityTntPrimed, SoundCategory::Blocks, &spawn_pos); + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0); + } + + async fn dispense_spawn_egg(ctx: &DispenseContext<'_>, item: &mut ItemStack) { + let Some(entity_type) = entity_from_egg(item.item.id) else { + return; + }; + + let _ = item.split(1); + let spawn_pos = Self::target_position(ctx).to_f64(); + + let mob = from_type(entity_type, spawn_pos, ctx.world, Uuid::new_v4()); + let yaw = wrap_degrees(rng().random::() * 360.0) % 360.0; + mob.get_entity().set_rotation(yaw, 0.0); + apply_entity_variant(item, mob.as_ref()); + + ctx.world.spawn_entity(mob).await; + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0); + } + + async fn drop_item(ctx: &DispenseContext<'_>, item: &mut ItemStack) { + let drop_item = item.split(1); + let facing = to_normal(ctx.facing); + let mut position = ctx.position.to_centered_f64().add(&(facing * 0.7)); + + position.y -= match ctx.facing { + Facing::Up | Facing::Down => 0.125, + _ => 0.15625, + }; + + let entity = Entity::new(ctx.world.clone(), position, &EntityType::ITEM); + let rd = rng().random::().mul_add(0.1, 0.2); + + let velocity = Vector3::new( + triangle(&mut rng(), facing.x * rd, 0.017_227_5 * 6.), + triangle(&mut rng(), 0.2, 0.017_227_5 * 6.), + triangle(&mut rng(), facing.z * rd, 0.017_227_5 * 6.), + ); + + let item_entity = Arc::new(ItemEntity::new_with_velocity( + entity, drop_item, velocity, 40, + )); + ctx.world.spawn_entity(item_entity).await; + + ctx.world + .sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0); + + ctx.world.sync_world_event( + WorldEvent::ParticlesShootSmoke, + *ctx.position, + to_data3d(ctx.facing), + ); + } +} diff --git a/pumpkin/src/item/items/boat.rs b/pumpkin/src/item/items/boat.rs index 0a1e77829..5f608f627 100644 --- a/pumpkin/src/item/items/boat.rs +++ b/pumpkin/src/item/items/boat.rs @@ -20,7 +20,7 @@ pub struct BoatItem; impl BoatItem { /// Maps boat item to corresponding entity type - fn item_to_entity(item: &Item) -> &'static EntityType { + pub(crate) fn item_to_entity(item: &Item) -> &'static EntityType { match item.id { val if val == Item::OAK_BOAT.id => &EntityType::OAK_BOAT, val if val == Item::OAK_CHEST_BOAT.id => &EntityType::OAK_CHEST_BOAT, diff --git a/pumpkin/src/item/items/spawn_egg.rs b/pumpkin/src/item/items/spawn_egg.rs index da7a6084e..4c09e84bc 100644 --- a/pumpkin/src/item/items/spawn_egg.rs +++ b/pumpkin/src/item/items/spawn_egg.rs @@ -27,7 +27,7 @@ impl ItemMetadata for SpawnEggItem { } } -fn apply_entity_variant(item: &ItemStack, mob: &dyn EntityBase) { +pub(crate) fn apply_entity_variant(item: &ItemStack, mob: &dyn EntityBase) { if let Some(comp) = item.get_data_component::() { mob.set_variant_name(&comp.value); } else if let Some(comp) = item.get_data_component::() {