Pumpkin/crates/pumpkin/src/item/items/bucket.rs
2026-08-09 21:36:03 +02:00

437 lines
13 KiB
Rust

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<f64>, Vector3<f64>) {
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<bool> {
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<World>,
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<World>, player: &Player) {
world.play_sound_raw(
Sound::BlockFireExtinguish as u16,
SoundCategory::Blocks,
&player.position(),
0.5,
(rand::random::<f32>() - rand::random::<f32>()).mul_add(0.8, 2.6),
);
}
async fn try_place_powder_snow(
world: &Arc<World>,
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<World>,
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<Box<dyn Future<Output = ()> + 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<World>| {
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<Box<dyn Future<Output = ()> + 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<World>| {
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<Box<dyn Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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
}
}