437 lines
13 KiB
Rust
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
|
|
}
|
|
}
|