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
This commit is contained in:
BouncingElf10
2026-07-25 09:36:10 +02:00
committed by GitHub
parent 56de61783a
commit 0dfaf5d213
3 changed files with 268 additions and 37 deletions

View File

@@ -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<World>,
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<R: Rng>(rng: &mut R, min: f64, max: f64) -> f64 {
(rng.random::<f64>() - rng.random::<f64>()).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::<f64>().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<dyn EntityBase> = 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<f64>,
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::<f32>() * 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::<f64>().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),
);
}
}

View File

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

View File

@@ -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::<ChickenVariantImpl>() {
mob.set_variant_name(&comp.value);
} else if let Some(comp) = item.get_data_component::<FrogVariantImpl>() {