use std::{pin::Pin, sync::Arc}; use crate::{ entity::player::Player, item::{ItemBehaviour, ItemMetadata}, }; use pumpkin_data::{ Block, BlockDirection, BlockStateId, dimension::Dimension, fluid::Fluid, item::Item, item_stack::ItemStack, sound::{Sound, SoundCategory}, }; use pumpkin_util::{ GameMode, math::{position::BlockPos, vector3::Vector3}, }; use pumpkin_world::{inventory::Inventory, tick::TickPriority, world::BlockFlags}; use crate::world::World; pub struct EmptyBucketItem; pub struct FilledBucketItem; pub struct MilkBucketItem; impl ItemMetadata for EmptyBucketItem { fn ids() -> Box<[u16]> { [Item::BUCKET.id].into() } } impl ItemMetadata for FilledBucketItem { fn ids() -> Box<[u16]> { [ Item::WATER_BUCKET.id, Item::LAVA_BUCKET.id, Item::POWDER_SNOW_BUCKET.id, Item::AXOLOTL_BUCKET.id, Item::COD_BUCKET.id, Item::SALMON_BUCKET.id, Item::TROPICAL_FISH_BUCKET.id, Item::PUFFERFISH_BUCKET.id, Item::TADPOLE_BUCKET.id, ] .into() } } impl ItemMetadata for MilkBucketItem { fn ids() -> Box<[u16]> { [Item::MILK_BUCKET.id].into() } } fn get_start_and_end_pos(player: &Player) -> (Vector3, Vector3) { let start_pos = player.eye_position(); let (yaw, pitch) = player.rotation(); let (yaw_rad, pitch_rad) = (f64::from(yaw.to_radians()), f64::from(pitch.to_radians())); let block_interaction_range = 4.5; // This is not the same as the block_interaction_range in the // player entity. let direction = Vector3::new( -yaw_rad.sin() * pitch_rad.cos() * block_interaction_range, -pitch_rad.sin() * block_interaction_range, pitch_rad.cos() * yaw_rad.cos() * block_interaction_range, ); let end_pos = start_pos.add(&direction); (start_pos, end_pos) } fn waterlogged_check(block: &Block, state: BlockStateId) -> Option { block.properties(state).and_then(|properties| { properties .to_props() .into_iter() .find(|p| p.0 == "waterlogged") .map(|(_, value)| value == "true") }) } fn is_waterlogged(block: &Block, state: BlockStateId) -> bool { waterlogged_check(block, state).unwrap_or(false) } fn set_waterlogged(block: &Block, state: BlockStateId, waterlogged: bool) -> BlockStateId { let Some(props) = block.properties(state) else { return state; }; let original_props = &props.to_props(); let waterlogged = waterlogged.to_string(); let props: Vec<(&str, &str)> = original_props .iter() .map(|(key, value)| { if *key == "waterlogged" { ("waterlogged", waterlogged.as_str()) } else { (*key, *value) } }) .collect(); block.from_properties(&props).to_state_id(block) } async fn give_player_bucket_item(player: &Player, item: &'static Item) { if player.gamemode.load() == GameMode::Creative { let inv = player.inventory.main_inventory.read().await; for stack in inv.iter() { if stack.item.id == item.id { return; } } drop(inv); let mut item_stack = ItemStack::new(1, item); player .inventory .insert_stack_anywhere(&mut item_stack) .await; } else { let item_stack = ItemStack::new(1, item); let mut held_stack = player.inventory.held_item().await; if held_stack.item_count == 1 { player.inventory.set_held_item(item_stack).await; } else { held_stack.decrement(1); player.inventory.set_held_item(held_stack).await; player .inventory .offer_or_drop_stack(item_stack, player) .await; } } } async fn try_pickup_bucket_item( world: &Arc, block_pos: BlockPos, direction: BlockDirection, ) -> Option<&'static Item> { let (block, state) = world.get_block_and_state_id(&block_pos); if block == &Block::POWDER_SNOW { world .break_block( &block_pos, None, BlockFlags::NOTIFY_ALL | BlockFlags::SKIP_DROPS, ) .await; return Some(&Item::POWDER_SNOW_BUCKET); } if is_waterlogged(block, state) { let state_id = set_waterlogged(block, state, false); world .set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS) .await; world.schedule_fluid_tick(&Fluid::WATER, block_pos, 5, TickPriority::Normal); return Some(&Item::WATER_BUCKET); } if state == Block::LAVA.default_state.id || state == Block::WATER.default_state.id { world .break_block(&block_pos, None, BlockFlags::NOTIFY_NEIGHBORS) .await; world .set_block_state( &block_pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_NEIGHBORS, ) .await; return Some(if state == Block::LAVA.default_state.id { &Item::LAVA_BUCKET } else { &Item::WATER_BUCKET }); } let target_pos = block_pos.offset(direction.to_offset()); let (block, state) = world.get_block_and_state_id(&target_pos); if waterlogged_check(block, state).is_some() { let state_id = set_waterlogged(block, state, false); world .set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS) .await; world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal); return Some(&Item::WATER_BUCKET); } None } fn should_evaporate_in_nether(item: &Item, world: &World) -> bool { item.id != Item::LAVA_BUCKET.id && item.id != Item::POWDER_SNOW_BUCKET.id && world.dimension == Dimension::THE_NETHER } fn play_bucket_evaporation(world: &Arc, player: &Player) { world.play_sound_raw( Sound::BlockFireExtinguish as u16, SoundCategory::Blocks, &player.position(), 0.5, (rand::random::() - rand::random::()).mul_add(0.8, 2.6), ); } async fn try_place_powder_snow( world: &Arc, pos: BlockPos, direction: BlockDirection, ) -> bool { let state = world.get_block_state(&pos); let target_pos = if state.replaceable() { pos } else { pos.offset(direction.to_offset()) }; let target_state = world.get_block_state(&target_pos); if !target_state.is_air() && !target_state.is_liquid() && !target_state.replaceable() { return false; } world .set_block_state( &target_pos, Block::POWDER_SNOW.default_state.id, BlockFlags::NOTIFY_NEIGHBORS, ) .await; true } async fn try_place_filled_bucket( world: &Arc, item: &Item, pos: BlockPos, direction: BlockDirection, ) -> bool { let (block, state) = world.get_block_and_state(&pos); if item.id == Item::POWDER_SNOW_BUCKET.id { return try_place_powder_snow(world, pos, direction).await; } if is_waterlogged(block, state.id) && item.id == Item::WATER_BUCKET.id { let state_id = set_waterlogged(block, state.id, true); world .set_block_state(&pos, state_id, BlockFlags::NOTIFY_NEIGHBORS) .await; world.schedule_fluid_tick(&Fluid::WATER, pos, 5, TickPriority::Normal); return true; } let target_pos = pos.offset(direction.to_offset()); let (block, state) = world.get_block_and_state(&target_pos); if waterlogged_check(block, state.id).is_some() { if item.id == Item::LAVA_BUCKET.id { return false; } let state_id = set_waterlogged(block, state.id, true); world .set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS) .await; world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal); return true; } if state.id == Block::AIR.default_state.id || state.is_liquid() { world .set_block_state( &target_pos, if item.id == Item::LAVA_BUCKET.id { Block::LAVA.default_state.id } else { Block::WATER.default_state.id }, BlockFlags::NOTIFY_NEIGHBORS, ) .await; return true; } false } impl ItemBehaviour for EmptyBucketItem { fn normal_use<'a>( &'a self, _block: &'a Item, player: &'a Player, ) -> Pin + Send + 'a>> { Box::pin(async move { let world = player.world(); let (start_pos, end_pos) = get_start_and_end_pos(player); let checker = async |pos: &BlockPos, world_inner: &Arc| { let state_id = world_inner.get_block_state_id(pos); let block = Block::from_state_id(state_id); if state_id == Block::AIR.default_state.id { return false; } (block.id != Block::WATER.id && block.id != Block::LAVA.id) || ((block.id == Block::WATER.id && state_id == Block::WATER.default_state.id) || (block.id == Block::LAVA.id && state_id == Block::LAVA.default_state.id)) }; let Some((block_pos, direction)) = world.raycast(start_pos, end_pos, checker).await else { return; }; let Some(item) = try_pickup_bucket_item(&world, block_pos, direction).await else { return; }; if let Some(server) = world.server.upgrade() && let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id) { let mut event = crate::plugin::api::events::player::player_bucket::PlayerBucketFillEvent::new( player_arc, block_pos, item.registry_key.to_string(), ); server.plugin_manager.fire(&server, &mut event).await; if event.cancelled { return; } } give_player_bucket_item(player, item).await; }) } fn as_any(&self) -> &dyn std::any::Any { self } } impl ItemBehaviour for FilledBucketItem { fn normal_use<'a>( &'a self, item: &'a Item, player: &'a Player, ) -> Pin + Send + 'a>> { Box::pin(async move { let world = player.world(); let (start_pos, end_pos) = get_start_and_end_pos(player); let checker = async |pos: &BlockPos, world_inner: &Arc| { let state_id = world_inner.get_block_state_id(pos); if Fluid::from_state_id(state_id).is_some() { return false; } state_id != Block::AIR.default_state.id }; let Some((pos, direction)) = world.raycast(start_pos, end_pos, checker).await else { return; }; if should_evaporate_in_nether(item, &world) { play_bucket_evaporation(&world, player); return; } if !try_place_filled_bucket(&world, item, pos, direction).await { return; } if let Some(server) = world.server.upgrade() && let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id) { let mut event = crate::plugin::api::events::player::player_bucket::PlayerBucketEmptyEvent::new( player_arc, pos, item.registry_key.to_string(), ); server.plugin_manager.fire(&server, &mut event).await; } //TODO: Spawn entity if applicable if player.gamemode.load() != GameMode::Creative { let item_stack = ItemStack::new(1, &Item::BUCKET); player .inventory .set_stack(player.inventory.get_selected_slot().into(), item_stack) .await; } }) } fn as_any(&self) -> &dyn std::any::Any { self } } impl ItemBehaviour for MilkBucketItem { fn normal_use<'a>( &'a self, _item: &'a Item, player: &'a Player, ) -> Pin + Send + 'a>> { Box::pin(async move { let stack = player.inventory().held_item().await; player .living_entity .set_active_hand(pumpkin_util::Hand::Right, stack, 32) .await; }) } fn on_stopped_using<'a>( &'a self, _stack: &'a ItemStack, player: &'a Player, ) -> Pin + Send + 'a>> { Box::pin(async move { player.living_entity.reset_effects_and_attributes().await; give_player_bucket_item(player, &Item::BUCKET).await; }) } fn get_use_duration(&self) -> i32 { 32 } fn as_any(&self) -> &dyn std::any::Any { self } }