Pumpkin/crates/pumpkin/src/block/blocks/redstone/dispenser.rs
2026-08-26 16:45:10 +02:00

798 lines
28 KiB
Rust

use rand::{Rng, RngExt, rng};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::block::blocks::redstone::block_receives_redstone_power;
use crate::block::blocks::tnt::TNTBlock;
use crate::block::registry::BlockActionResult;
use crate::block::{
BlockBehaviour, GetComparatorOutputArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs,
OnScheduledTickArgs, PlacedArgs,
};
use crate::entity::decoration::armor_stand::ArmorStandEntity;
use crate::entity::item::ItemEntity;
use crate::entity::projectile::ThrownItemEntity;
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
use crate::entity::projectile::egg::EggEntity;
use crate::entity::projectile::firework_rocket::FireworkRocketEntity;
use crate::entity::projectile::lingering_potion::LingeringPotionEntity;
use crate::entity::projectile::small_fireball::SmallFireballEntity;
use crate::entity::projectile::snowball::SnowballEntity;
use crate::entity::projectile::splash_potion::SplashPotionEntity;
use crate::entity::projectile::wind_charge::{WIND_CHARGE_GRAVITY, WindChargeEntity};
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::bucket::{
FilledBucketItem, play_bucket_evaporation, should_evaporate_in_nether, try_pickup_fluid_at,
try_place_filled_bucket,
};
use crate::item::items::honeycomb::try_wax_block;
use crate::item::items::ignite::ignition::Ignition;
use crate::item::items::spawn_egg::apply_entity_variant;
use crate::world::World;
use crate::block::entities::dispenser::DispenserBlockEntity;
use pumpkin_data::block_properties::{BlockProperties, Facing};
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_data::{Block, BlockStateId, FacingExt};
use pumpkin_inventory::generic_container_screen_handler::create_generic_3x3;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
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;
use pumpkin_world::world::BlockFlags;
struct DispenserScreenFactory(Arc<dyn Inventory>);
impl ScreenHandlerFactory for DispenserScreenFactory {
fn create_screen_handler<'a>(
&'a self,
sync_id: u8,
player_inventory: &'a Arc<PlayerInventory>,
_player: &'a dyn InventoryPlayer,
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
Box::pin(async move {
let handler = create_generic_3x3(sync_id, player_inventory, self.0.clone()).await;
let screen_handler_arc = Arc::new(Mutex::new(handler));
Some(screen_handler_arc as SharedScreenHandler)
})
}
fn get_display_name(&self) -> TextComponent {
pumpkin_macros::translate_cross!(
translation::java::CONTAINER_DISPENSER,
translation::bedrock::CONTAINER_DISPENSER
)
}
}
#[pumpkin_block("minecraft:dispenser")]
pub struct DispenserBlock;
type DispenserLikeProperties = pumpkin_data::block_properties::DispenserLikeProperties;
struct DispenseContext<'a> {
world: &'a Arc<World>,
position: &'a BlockPos,
facing: Facing,
}
fn triangle<R: Rng>(rng: &mut R, min: f64, max: f64) -> f64 {
(rng.random::<f64>() - rng.random::<f64>()).mul_add(max, min)
}
const fn to_normal(facing: Facing) -> Vector3<f64> {
match facing {
Facing::North => Vector3::new(0., 0., -1.),
Facing::East => Vector3::new(1., 0., 0.),
Facing::South => Vector3::new(0., 0., 1.),
Facing::West => Vector3::new(-1., 0., 0.),
Facing::Up => Vector3::new(0., 1., 0.),
Facing::Down => Vector3::new(0., -1., 0.),
}
}
const fn to_data3d(facing: Facing) -> i32 {
match facing {
Facing::North => 2,
Facing::East => 5,
Facing::South => 3,
Facing::West => 4,
Facing::Up => 1,
Facing::Down => 0,
}
}
impl BlockBehaviour for DispenserBlock {
fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(inventory) = block_entity.get_inventory()
{
let player = Arc::clone(args.player);
let pos = *args.position;
tokio::spawn(async move {
player
.open_handled_screen(&DispenserScreenFactory(inventory), Some(pos))
.await;
});
}
BlockActionResult::Success
}
fn on_place(&self, args: OnPlaceArgs<'_>) -> BlockStateId {
let mut props = DispenserLikeProperties::default(args.block);
props.facing = args.player.get_entity().get_facing().opposite();
props.to_state_id(args.block)
}
fn placed(&self, args: PlacedArgs<'_>) {
let dispenser_block_entity = DispenserBlockEntity::new(*args.position);
args.world
.add_block_entity(Arc::new(dispenser_block_entity));
}
fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
let powered = block_receives_redstone_power(args.world, args.position)
|| block_receives_redstone_power(args.world, &args.position.up());
let mut props = DispenserLikeProperties::from_state_id(
args.world.get_block_state(args.position).id,
args.block,
);
if powered && !props.triggered {
args.world
.schedule_block_tick(args.block, *args.position, 4, TickPriority::Normal);
props.triggered = true;
args.world.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_LISTENERS,
);
} else if !powered && props.triggered {
props.triggered = false;
args.world.set_block_state(
args.position,
props.to_state_id(args.block),
BlockFlags::NOTIFY_LISTENERS,
);
}
}
fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) {
let world = args.world.clone();
let position = *args.position;
tokio::spawn(async move {
let (block, state) = world.get_block_and_state(&position);
if let Some(block_entity) = world.get_block_entity(&position) {
let Some(dispenser) = block_entity.as_any().downcast_ref::<DispenserBlockEntity>()
else {
return;
};
if let Some((slot_index, mut item)) = dispenser.get_random_slot().await {
let props = DispenserLikeProperties::from_state_id(state.id, block);
let ctx = DispenseContext {
world: &world,
position: &position,
facing: props.facing,
};
Self::dispense(&ctx, dispenser, &mut item).await;
dispenser.set_stack(slot_index, item).await;
} else {
world.sync_world_event(WorldEvent::SoundDispenserFail, position, 0);
}
}
});
}
fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option<u8> {
if let Some(block_entity) = args.world.get_block_entity(args.position)
&& let Some(inventory) = block_entity.get_inventory()
{
Some(crate::block::calculate_comparator_output(
inventory.as_ref(),
))
} else {
None
}
}
}
impl DispenserBlock {
// Velocity values match the vanilla dispenser projectile settings.
const DEFAULT_PROJECTILE_POWER: f64 = 1.1;
const DEFAULT_PROJECTILE_UNCERTAINTY: f64 = 6.0;
const POTION_PROJECTILE_POWER: f64 = 1.375;
const POTION_PROJECTILE_UNCERTAINTY: f64 = 3.0;
// Fire charges and wind charges share these values.
const FIREBALL_PROJECTILE_POWER: f64 = 1.0;
const FIREBALL_PROJECTILE_UNCERTAINTY: f64 = 6.666_666_5;
const FIREWORK_PROJECTILE_POWER: f64 = 0.5;
const FIREWORK_PROJECTILE_UNCERTAINTY: f64 = 1.0;
async fn dispense(
ctx: &DispenseContext<'_>,
dispenser: &DispenserBlockEntity,
item: &mut ItemStack,
) {
let mut event = crate::plugin::api::events::block::block_dispense::BlockDispenseEvent::new(
*ctx.position,
item.item.registry_key.to_string(),
);
if let Some(server) = ctx.world.server.upgrade() {
server.plugin_manager.fire(&server, &mut event).await;
}
if event.cancelled {
ctx.world
.sync_world_event(WorldEvent::SoundDispenserFail, *ctx.position, 0);
return;
}
// 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();
if arrows.contains(&item.item.id) {
// Arrows
Self::fire_arrow(ctx, item);
} else if boats.contains(&item.item.id) {
// Boats
if !Self::dispense_boat(ctx, item) {
Self::drop_item(ctx, item);
}
} else if item.item.id == Item::ARMOR_STAND.id {
// Armor stands
if !Self::dispense_armor_stand(ctx, item) {
Self::drop_item(ctx, item);
}
} else if item.item.id == Item::TNT.id {
// TNT
Self::dispense_tnt(ctx, item);
} else if item.item.id == Item::SNOWBALL.id {
Self::dispense_snowball(ctx, item);
} else if item.item.id == Item::EGG.id {
Self::dispense_egg(ctx, item);
} else if item.item.id == Item::SPLASH_POTION.id {
Self::dispense_splash_potion(ctx, item);
} else if item.item.id == Item::LINGERING_POTION.id {
Self::dispense_lingering_potion(ctx, item);
} else if item.item.id == Item::FIRE_CHARGE.id {
Self::dispense_fire_charge(ctx, item);
} else if item.item.id == Item::WIND_CHARGE.id {
Self::dispense_wind_charge(ctx, item);
} else if item.item.id == Item::FIREWORK_ROCKET.id {
Self::dispense_firework_rocket(ctx, item);
} else if item.item.id == Item::BUCKET.id {
// Empty buckets pick up the fluid in front of the dispenser
Self::dispense_empty_bucket(ctx, dispenser, item).await;
} else if FilledBucketItem::ids().contains(&item.item.id) {
// Filled buckets place their fluid in front of the dispenser
Self::dispense_filled_bucket(ctx, item).await;
} else if item.item.id == Item::FLINT_AND_STEEL.id {
// Flint and steel light fires and prime TNT
Self::dispense_flint_and_steel(ctx, item).await;
} else if item.item.id == Item::HONEYCOMB.id {
// Honeycombs wax copper blocks
Self::dispense_honeycomb(ctx, item).await;
} else if entity_from_egg(item.item.id).is_some() {
// Spawn eggs
Self::dispense_spawn_egg(ctx, item);
} else {
// Default / Drop
Self::drop_item(ctx, item);
}
}
fn projectile_spawn_position(ctx: &DispenseContext<'_>) -> Vector3<f64> {
ctx.position
.to_centered_f64()
.add(&(to_normal(ctx.facing) * 0.7))
}
fn launch_thrown(
ctx: &DispenseContext<'_>,
thrown: &ThrownItemEntity,
power: f64,
uncertainty: f64,
) {
let facing = to_normal(ctx.facing);
thrown.set_velocity(facing.x, facing.y + 0.1, facing.z, power, uncertainty);
}
fn finish_projectile_launch(
ctx: &DispenseContext<'_>,
projectile: Arc<dyn EntityBase>,
launch_event: WorldEvent,
) {
ctx.world.spawn_entity(projectile);
Self::play_dispense_effects(ctx, launch_event);
}
fn play_dispense_effects(ctx: &DispenseContext<'_>, sound_event: WorldEvent) {
ctx.world.sync_world_event(sound_event, *ctx.position, 0);
ctx.world.sync_world_event(
WorldEvent::ParticlesShootSmoke,
*ctx.position,
to_data3d(ctx.facing),
);
}
fn fire_arrow(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let projectile = item.split(1);
let facing = to_normal(ctx.facing);
let arrow_entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
ArrowEntity::entity_type_for_item(projectile.item),
);
let arrow =
ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed);
arrow.set_velocity(
facing.x,
facing.y + 0.1,
facing.z,
Self::DEFAULT_PROJECTILE_POWER,
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(arrow),
WorldEvent::SoundDispenserProjectileLaunch,
);
}
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()
}
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)));
ctx.world
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
true
}
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)));
ctx.world
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
true
}
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);
ctx.world
.play_sound(Sound::EntityTntPrimed, SoundCategory::Blocks, &spawn_pos);
ctx.world
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
}
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);
ctx.world
.sync_world_event(WorldEvent::SoundDispenserDispense, *ctx.position, 0);
}
fn dispense_snowball(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let _ = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::SNOWBALL,
);
let snowball = SnowballEntity::new(entity);
Self::launch_thrown(
ctx,
&snowball.thrown,
Self::DEFAULT_PROJECTILE_POWER,
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(snowball),
WorldEvent::SoundDispenserProjectileLaunch,
);
}
fn dispense_egg(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let projectile = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::EGG,
);
let egg = EggEntity::new(entity);
egg.set_item_stack(projectile);
Self::launch_thrown(
ctx,
&egg.thrown,
Self::DEFAULT_PROJECTILE_POWER,
Self::DEFAULT_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(egg),
WorldEvent::SoundDispenserProjectileLaunch,
);
}
fn dispense_splash_potion(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let projectile = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::SPLASH_POTION,
);
let potion = SplashPotionEntity::new(entity);
potion.set_item_stack(projectile);
Self::launch_thrown(
ctx,
&potion.thrown,
Self::POTION_PROJECTILE_POWER,
Self::POTION_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(potion),
WorldEvent::SoundDispenserProjectileLaunch,
);
}
fn dispense_lingering_potion(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let projectile = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::LINGERING_POTION,
);
let potion = LingeringPotionEntity::new(entity);
potion.set_item_stack(projectile);
Self::launch_thrown(
ctx,
&potion.thrown,
Self::POTION_PROJECTILE_POWER,
Self::POTION_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(potion),
WorldEvent::SoundDispenserProjectileLaunch,
);
}
fn dispense_fire_charge(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let _ = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::SMALL_FIREBALL,
);
let fireball = SmallFireballEntity::new(entity);
// Vanilla aims fire charges straight along the facing axis, without the +0.1 Y bias
// other projectiles get.
let facing = to_normal(ctx.facing);
fireball.thrown.set_velocity(
facing.x,
facing.y,
facing.z,
Self::FIREBALL_PROJECTILE_POWER,
Self::FIREBALL_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(ctx, Arc::new(fireball), WorldEvent::SoundBlazeFireball);
}
fn dispense_wind_charge(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let _ = item.split(1);
let entity = Entity::new(
ctx.world.clone(),
Self::projectile_spawn_position(ctx),
&EntityType::WIND_CHARGE,
);
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: WIND_CHARGE_GRAVITY,
};
Self::launch_thrown(
ctx,
&thrown,
Self::FIREBALL_PROJECTILE_POWER,
Self::FIREBALL_PROJECTILE_UNCERTAINTY,
);
Self::finish_projectile_launch(
ctx,
Arc::new(WindChargeEntity::new_normal(thrown)),
WorldEvent::SoundWindChargeShoot,
);
}
fn dispense_firework_rocket(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let _ = item.split(1);
let facing = to_normal(ctx.facing);
// Vanilla spawns fireworks closer to the dispenser face and slightly above center.
let position = ctx
.position
.to_centered_f64()
.add(&(facing * (0.7 * 0.5125)))
.add(&Vector3::new(0.0, 0.08, 0.0));
let entity = Entity::new(ctx.world.clone(), position, &EntityType::FIREWORK_ROCKET);
let rocket = FireworkRocketEntity::new(entity);
// `FireworkRocketEntity` does not expose its inner projectile, so replicate
// `ThrownItemEntity::set_velocity` here.
let deviation = 0.017_227_5 * Self::FIREWORK_PROJECTILE_UNCERTAINTY;
let velocity = Vector3::new(facing.x, facing.y + 0.1, facing.z)
.normalize()
.add_raw(
triangle(&mut rng(), 0.0, deviation),
triangle(&mut rng(), 0.0, deviation),
triangle(&mut rng(), 0.0, deviation),
)
.multiply(
Self::FIREWORK_PROJECTILE_POWER,
Self::FIREWORK_PROJECTILE_POWER,
Self::FIREWORK_PROJECTILE_POWER,
);
let rocket_entity = rocket.get_entity();
rocket_entity.set_velocity(velocity);
rocket_entity.set_rotation(
velocity.x.atan2(velocity.z) as f32 * 57.295_776,
velocity.y.atan2(velocity.horizontal_length()) as f32 * 57.295_776,
);
Self::finish_projectile_launch(ctx, Arc::new(rocket), WorldEvent::SoundFireworkShoot);
}
async fn dispense_empty_bucket(
ctx: &DispenseContext<'_>,
dispenser: &DispenserBlockEntity,
item: &mut ItemStack,
) {
let front = Self::target_position(ctx);
let Some(filled) = try_pickup_fluid_at(ctx.world, front) else {
Self::drop_item(ctx, item);
return;
};
item.decrement(1);
let filled_stack = ItemStack::new(1, filled);
if item.is_empty() {
*item = filled_stack;
} else if let Some(rest) = Self::add_to_first_free_slot(dispenser, filled_stack).await {
Self::eject_item(ctx, rest);
}
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
}
/// Places `stack` into the first empty slot, returning it back if every slot is occupied.
/// The slot currently being dispensed from still holds its pre-dispense stack, so it is
/// never considered free.
async fn add_to_first_free_slot(
dispenser: &DispenserBlockEntity,
stack: ItemStack,
) -> Option<ItemStack> {
let mut items = dispenser.items.write().await;
for slot in items.iter_mut() {
if slot.is_empty() {
*slot = stack;
dispenser.mark_dirty();
return None;
}
}
Some(stack)
}
async fn dispense_filled_bucket(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let front = Self::target_position(ctx);
// TODO: Spawn the stored entity for axolotl/fish/tadpole buckets, like the player path.
let emptied = if should_evaporate_in_nether(item.item, ctx.world) {
play_bucket_evaporation(ctx.world, &front.to_f64());
true
} else {
try_place_filled_bucket(
ctx.world,
item.item,
*ctx.position,
ctx.facing.to_block_direction(),
)
};
if emptied {
*item = ItemStack::new(1, &Item::BUCKET);
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
} else {
Self::drop_item(ctx, item);
}
}
async fn dispense_flint_and_steel(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let front = Self::target_position(ctx);
let front_block = ctx.world.get_block(&front);
let ignited = if front_block == &Block::TNT {
TNTBlock::prime(ctx.world, &front);
true
} else {
Ignition::ignite_block(
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {
world.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL);
},
ctx.world,
front,
front,
front_block,
)
};
if ignited {
// `damage_item` already consumes the tool from the stack when it breaks.
let _ = item.damage_item(1);
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
} else {
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserFail);
}
}
async fn dispense_honeycomb(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let front = Self::target_position(ctx);
let front_block = ctx.world.get_block(&front);
if try_wax_block(ctx.world, front, front_block) {
item.decrement(1);
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
} else {
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserFail);
}
}
fn drop_item(ctx: &DispenseContext<'_>, item: &mut ItemStack) {
let drop_item = item.split(1);
Self::eject_item(ctx, drop_item);
Self::play_dispense_effects(ctx, WorldEvent::SoundDispenserDispense);
}
fn eject_item(ctx: &DispenseContext<'_>, stack: ItemStack) {
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, stack, velocity, 40));
ctx.world.spawn_entity(item_entity);
}
}