Add buckets with fluids (#736)

* Buckets start

* buckets in survival

* lint

* fmt

* lint

* fix clippy allow

* fix creative inventory for buckets

* fmt, lint

* remove logs

* fix placing lava

* lint

* fix issues

* fmt

* bruh

* Fix comments

* code quality
This commit is contained in:
teknostom
2025-05-06 18:04:57 +02:00
committed by GitHub
parent afe42a5d15
commit 1b78299193
6 changed files with 359 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
use bytes::BufMut;
use std::ops::{Add, AddAssign, Div, Mul, Sub};
use num_traits::Float;
use num_traits::{Float, Num};
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Default)]
pub struct Vector3<T> {
@@ -10,7 +10,7 @@ pub struct Vector3<T> {
pub z: T,
}
impl<T: Math + Copy> Vector3<T> {
impl<T: Math + PartialOrd + Copy> Vector3<T> {
pub const fn new(x: T, y: T, z: T) -> Self {
Vector3 { x, y, z }
}
@@ -55,6 +55,43 @@ impl<T: Math + Copy> Vector3<T> {
}
}
pub fn lerp(&self, other: &Vector3<T>, t: T) -> Self {
Vector3 {
x: self.x + (other.x - self.x) * t,
y: self.y + (other.y - self.y) * t,
z: self.z + (other.z - self.z) * t,
}
}
pub fn sign(&self) -> Vector3<i32>
where
T: Num + PartialOrd + Copy,
{
Vector3 {
x: if self.x > T::zero() {
1
} else if self.x < T::zero() {
-1
} else {
0
},
y: if self.y > T::zero() {
1
} else if self.y < T::zero() {
-1
} else {
0
},
z: if self.z > T::zero() {
1
} else if self.z < T::zero() {
-1
} else {
0
},
}
}
pub fn squared_distance_to_vec(&self, other: Self) -> T {
self.squared_distance_to(other.x, other.y, other.z)
}

View File

@@ -40,7 +40,7 @@ use pumpkin_config::{BASIC_CONFIG, advanced_config};
use pumpkin_data::{
BlockState,
damage::DamageType,
entity::{EffectType, EntityStatus, EntityType},
entity::{EffectType, EntityPose, EntityStatus, EntityType},
item::Operation,
particle::Particle,
sound::{Sound, SoundCategory},
@@ -716,6 +716,26 @@ impl Player {
self.living_entity.entity.pos.load()
}
pub fn eye_position(&self) -> Vector3<f64> {
let eye_height = if self.living_entity.entity.pose.load() == EntityPose::Crouching {
1.27
} else {
f64::from(self.living_entity.entity.standing_eye_height)
};
Vector3::new(
self.living_entity.entity.pos.load().x,
self.living_entity.entity.pos.load().y + eye_height,
self.living_entity.entity.pos.load().z,
)
}
pub fn rotation(&self) -> (f32, f32) {
(
self.living_entity.entity.yaw.load(),
self.living_entity.entity.pitch.load(),
)
}
/// Updates the current abilities the player has.
pub async fn send_abilities_update(&self) {
let mut b = 0i8;

View File

@@ -0,0 +1,183 @@
use std::sync::Arc;
use crate::entity::player::Player;
use async_trait::async_trait;
use pumpkin_data::Block;
use pumpkin_data::fluid::Fluid;
use pumpkin_data::item::Item;
use pumpkin_inventory::player::PlayerInventory;
use pumpkin_protocol::client::play::CSetContainerSlot;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_util::GameMode;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
use crate::item::pumpkin_item::{ItemMetadata, PumpkinItem};
use crate::world::{BlockFlags, World};
pub struct EmptyBucketItem;
pub struct FilledBucketItem;
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,
// TODO drink milk
// Item::MILK_BUCKET.id,
// TODO implement these buckets, and getting the item from the world
// 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()
}
}
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)
}
#[async_trait]
impl PumpkinItem for EmptyBucketItem {
#[allow(clippy::too_many_lines)]
async fn normal_use(&self, _item: &Item, player: &Player) {
let world = player.world().await.clone();
let (start_pos, end_pos) = get_start_and_end_pos(player);
let checker = async |pos: &BlockPos, world_inner: &Arc<World>| {
let Ok(state_id) = world_inner.get_block_state_id(pos).await else {
return false;
};
state_id == Block::WATER.default_state_id || state_id == Block::LAVA.default_state_id
};
let (block_pos, _) = world.raytrace(start_pos, end_pos, checker).await;
if let Some(pos) = block_pos {
let Ok(state_id) = world.get_block_state_id(&pos).await else {
return;
};
world
.set_block_state(&pos, Block::AIR.id, BlockFlags::NOTIFY_NEIGHBORS)
.await;
let mut inventory = player.inventory().lock().await;
let selected = inventory.get_selected_slot();
let item_type = if state_id == Block::WATER.default_state_id {
Item::WATER_BUCKET
} else {
Item::LAVA_BUCKET
};
let item_stack = Some(ItemStack::new(1, item_type.clone()));
let slot_data = ItemStackSerializer::from(item_stack.clone());
let game_mode = player.gamemode.load();
if game_mode == GameMode::Creative {
let slot = inventory.get_pickup_item_slot(item_type.id);
if let Some(slot) = slot {
if let Err(err) = inventory.set_slot(slot, item_stack, false) {
log::error!("Failed to set slot: {err}");
} else {
let dest_packet = CSetContainerSlot::new(
PlayerInventory::CONTAINER_ID,
inventory.state_id as i32,
slot as i16,
&slot_data,
);
player.client.enqueue_packet(&dest_packet).await;
}
}
} else if let Err(err) = inventory.set_slot(selected, item_stack.clone(), false) {
log::error!("Failed to set slot: {err}");
} else {
let dest_packet = CSetContainerSlot::new(
PlayerInventory::CONTAINER_ID,
inventory.state_id as i32,
selected as i16,
&slot_data,
);
player.client.enqueue_packet(&dest_packet).await;
}
}
}
}
#[async_trait]
impl PumpkinItem for FilledBucketItem {
async fn normal_use(&self, item: &Item, player: &Player) {
if item.id == Item::MILK_BUCKET.id {
// TODO implement milk bucket
return;
}
let world = player.world().await.clone();
let (start_pos, end_pos) = get_start_and_end_pos(player);
let checker = async |pos: &BlockPos, world_inner: &Arc<World>| {
let Ok(state_id) = world_inner.get_block_state_id(pos).await else {
return false;
};
if Fluid::from_state_id(state_id).is_some() {
return false;
}
state_id != Block::AIR.id
};
let (block_pos, block_direction) = world.raytrace(start_pos, end_pos, checker).await;
if let (Some(pos), Some(direction)) = (block_pos, block_direction) {
world
.set_block_state(
&pos.offset(direction.to_offset()),
// Block::WATER.default_state_id,
if item.id == Item::WATER_BUCKET.id {
Block::WATER.default_state_id
} else {
Block::LAVA.default_state_id
},
BlockFlags::NOTIFY_NEIGHBORS,
)
.await;
if player.gamemode.load() != GameMode::Creative {
let mut inventory = player.inventory().lock().await;
let selected = inventory.get_selected_slot();
let item = Some(ItemStack::new(1, Item::BUCKET));
let slot_data = ItemStackSerializer::from(item.clone());
if let Err(err) = inventory.set_slot(selected, item, false) {
log::error!("Failed to set slot: {err}");
} else {
let dest_packet = CSetContainerSlot::new(
PlayerInventory::CONTAINER_ID,
inventory.state_id as i32,
selected as i16,
&slot_data,
);
player.client.enqueue_packet(&dest_packet).await;
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
mod axe;
mod bucket;
mod egg;
mod flint_and_steel;
mod hoe;
@@ -9,6 +10,7 @@ mod sword;
mod trident;
use axe::AxeItem;
use bucket::{EmptyBucketItem, FilledBucketItem};
use egg::EggItem;
use flint_and_steel::FlintAndSteelItem;
use hoe::HoeItem;
@@ -30,6 +32,8 @@ pub fn default_registry() -> Arc<ItemRegistry> {
manager.register(FlintAndSteelItem);
manager.register(SwordItem);
manager.register(TridentItem);
manager.register(EmptyBucketItem);
manager.register(FilledBucketItem);
manager.register(ShovelItem);
manager.register(AxeItem);
manager.register(HoneyCombItem);

View File

@@ -1418,7 +1418,7 @@ impl Player {
.item_registry
.use_on_block(&stack.item, self, location, face, &block, server)
.await;
self.update_sequence(use_item_on.sequence.0);
let action_result = server
.block_registry
.use_with_item(&block, self, location, &stack.item, server, world)
@@ -1484,13 +1484,19 @@ impl Player {
world.add_block_entity(Arc::new(updated_sign)).await;
}
pub async fn handle_use_item(&self, _use_item: &SUseItem, server: &Server) {
pub async fn handle_use_item(&self, use_item: &SUseItem, server: &Server) {
if !self.has_client_loaded() {
return;
}
if let Some(held) = self.inventory().lock().await.held_item() {
server.item_registry.on_use(&held.item, self).await;
let held = {
let inventory = self.inventory().lock().await;
inventory.held_item().cloned()
};
if held.is_some() {
server.item_registry.on_use(&held.unwrap().item, self).await;
}
self.update_sequence(use_item.sequence.0);
}
pub async fn handle_set_held_item(&self, held: SSetHeldItem) {

View File

@@ -1729,4 +1729,106 @@ impl World {
chunk.block_entities.remove(block_pos);
chunk.dirty = true;
}
pub async fn raytrace(
self: &Arc<Self>,
start_pos: Vector3<f64>,
end_pos: Vector3<f64>,
hit_check: impl AsyncFn(&BlockPos, &Arc<Self>) -> bool,
) -> (Option<BlockPos>, Option<BlockDirection>) {
if start_pos == end_pos {
return (None, None);
}
let adjust = -1.0e-7f64;
let to = end_pos.lerp(&start_pos, adjust);
let from = start_pos.lerp(&end_pos, adjust);
let mut block = BlockPos::floored(from.x, from.y, from.z);
if hit_check(&block, self).await {
return (Some(block), None);
}
let difference = to.sub(&from);
let step = difference.sign();
let delta = Vector3::new(
if step.x == 0 {
f64::MAX
} else {
(f64::from(step.x)) / difference.x
},
if step.y == 0 {
f64::MAX
} else {
(f64::from(step.y)) / difference.y
},
if step.z == 0 {
f64::MAX
} else {
(f64::from(step.z)) / difference.z
},
);
let mut next = Vector3::new(
delta.x
* (if step.x > 0 {
1.0 - (from.x - from.x.floor())
} else {
from.x - from.x.floor()
}),
delta.y
* (if step.y > 0 {
1.0 - (from.y - from.y.floor())
} else {
from.y - from.y.floor()
}),
delta.z
* (if step.z > 0 {
1.0 - (from.z - from.z.floor())
} else {
from.z - from.z.floor()
}),
);
while next.x <= 1.0 || next.y <= 1.0 || next.z <= 1.0 {
let block_direction = match (next.x, next.y, next.z) {
(x, y, z) if x < y && x < z => {
block.0.x += step.x;
next.x += delta.x;
if step.x > 0 {
BlockDirection::West
} else {
BlockDirection::East
}
}
(_, y, z) if y < z => {
block.0.y += step.y;
next.y += delta.y;
if step.y > 0 {
BlockDirection::Down
} else {
BlockDirection::Up
}
}
_ => {
block.0.z += step.z;
next.z += delta.z;
if step.z > 0 {
BlockDirection::North
} else {
BlockDirection::South
}
}
};
if hit_check(&block, self).await {
return (Some(block), Some(block_direction));
}
}
(None, None)
}
}