From 4ec9db1e36ee00aaa30f7fd997b20121486b9f9e Mon Sep 17 00:00:00 2001 From: RB007 <36972202+RoosterBooster007@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:53:00 -0500 Subject: [PATCH] chore: optimized, working fluids (#1392) * fix: optimized, working fluids * chore: clippy and formatting --- pumpkin/src/block/blocks/doors.rs | 12 + pumpkin/src/block/fluid/flowing.rs | 680 ----------------------- pumpkin/src/block/fluid/flowing_trait.rs | 547 ++++++++++++++++++ pumpkin/src/block/fluid/lava.rs | 88 ++- pumpkin/src/block/fluid/mod.rs | 18 +- pumpkin/src/block/fluid/pathfinder.rs | 225 ++++++++ pumpkin/src/block/fluid/physics.rs | 51 ++ pumpkin/src/block/fluid/water.rs | 28 +- 8 files changed, 902 insertions(+), 747 deletions(-) delete mode 100644 pumpkin/src/block/fluid/flowing.rs create mode 100644 pumpkin/src/block/fluid/flowing_trait.rs create mode 100644 pumpkin/src/block/fluid/pathfinder.rs create mode 100644 pumpkin/src/block/fluid/physics.rs diff --git a/pumpkin/src/block/blocks/doors.rs b/pumpkin/src/block/blocks/doors.rs index e513b9d39..d96e9c3fd 100644 --- a/pumpkin/src/block/blocks/doors.rs +++ b/pumpkin/src/block/blocks/doors.rs @@ -224,6 +224,18 @@ impl BlockBehaviour for DoorBlock { let other_pos = args.position.offset(other_half.to_offset()); let (other_block, other_state_id) = args.world.get_block_and_state_id(&other_pos).await; + // Check if destroyed and notify (water) + if other_block.id != args.block.id { + args.world + .set_block_state( + args.position, + Block::AIR.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + return; + } + let powered = block_receives_redstone_power(args.world, args.position).await || block_receives_redstone_power(args.world, &other_pos).await; diff --git a/pumpkin/src/block/fluid/flowing.rs b/pumpkin/src/block/fluid/flowing.rs deleted file mode 100644 index ff0466fb4..000000000 --- a/pumpkin/src/block/fluid/flowing.rs +++ /dev/null @@ -1,680 +0,0 @@ -use pumpkin_data::tag::Taggable; -use pumpkin_data::{ - Block, BlockDirection, - fluid::{EnumVariants, Falling, Fluid, FluidProperties, Level}, - tag, -}; -use pumpkin_util::math::position::BlockPos; -use pumpkin_world::{BlockId, BlockStateId, tick::TickPriority, world::BlockFlags}; -use std::sync::Arc; -use std::{collections::HashMap, pin::Pin}; - -use crate::{block::BlockFuture, world::World}; -type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; - -#[derive(Clone)] -pub struct SpreadContext { - holes: HashMap, -} - -impl Default for SpreadContext { - fn default() -> Self { - Self::new() - } -} - -impl SpreadContext { - #[must_use] - pub fn new() -> Self { - Self { - holes: HashMap::new(), - } - } - pub async fn is_hole( - &mut self, - fluid: &T, - world: &Arc, - fluid_type: &Fluid, - pos: &BlockPos, - ) -> bool { - if let Some(is_hole) = self.holes.get(pos) { - return *is_hole; - } - - let below_pos = pos.down(); - let is_hole = fluid - .is_water_hole(world, fluid_type, pos, &below_pos) - .await; - - self.holes.insert(*pos, is_hole); - is_hole - } -} - -pub type FluidFuture<'a, T> = Pin + Send + 'a>>; - -pub trait FlowingFluid: Send + Sync { - fn get_level_decrease_per_block(&self, world: &World) -> i32; - - /// Get the tick delay for this fluid (how many ticks between flow updates) - fn get_flow_speed(&self, world: &World) -> u8; - - fn get_source<'a>( - &'a self, - fluid: &'a Fluid, - falling: bool, - ) -> FluidFuture<'a, FlowingFluidProperties> { - Box::pin(async move { - let mut source_props = FlowingFluidProperties::default(fluid); - source_props.level = Level::L8; - source_props.falling = if falling { - Falling::True - } else { - Falling::False - }; - source_props - }) - } - - fn get_flowing<'a>( - &'a self, - fluid: &'a Fluid, - level: Level, - falling: bool, - ) -> FluidFuture<'a, FlowingFluidProperties> { - Box::pin(async move { - let mut flowing_props = FlowingFluidProperties::default(fluid); - flowing_props.level = level; - flowing_props.falling = if falling { - Falling::True - } else { - Falling::False - }; - flowing_props - }) - } - - fn get_max_flow_distance(&self, world: &World) -> i32; - - fn can_convert_to_source(&self, world: &Arc) -> bool; - - fn is_waterlogged<'a>( - &'a self, - world: &'a Arc, - pos: &'a BlockPos, - ) -> FluidFuture<'a, Option> { - Box::pin(async move { - let block = world.get_block(pos).await; - - let state_id = world.get_block_state_id(pos).await; - // Check if the block has waterlogged property and if it's true - if let Some(properties) = block.properties(state_id) - && properties - .to_props() - .iter() - .any(|(key, value)| *key == "waterlogged" && *value == "true") - { - return Some(state_id); - } - None - }) - } - - fn is_same_fluid(&self, fluid: &Fluid, other_state_id: BlockStateId) -> bool { - if let Some(other_fluid) = Fluid::from_state_id(other_state_id) { - return fluid.id == other_fluid.id; - } - false - } - - fn on_scheduled_tick_internal<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - ) -> FluidFuture<'a, ()> { - Box::pin(async move { - let current_block_state = world.get_block_state(block_pos).await; - - // If the block at this position is no longer this fluid, ignore the scheduled tick. - let is_fluid_state_id = fluid - .states - .iter() - .any(|state| state.block_state_id == current_block_state.id); - if !is_fluid_state_id { - return; - } - - let current_fluid_state = - FlowingFluidProperties::from_state_id(current_block_state.id, fluid); - - // Only update non-source blocks (or falling source blocks) - let mut updated_fluid_state = current_fluid_state; - if current_fluid_state.level != Level::L8 - || current_fluid_state.falling == Falling::True - { - let new_fluid_state = self.get_new_liquid(world, fluid, block_pos).await; - if let Some(new_fluid_state) = new_fluid_state { - let new_state_id = new_fluid_state.to_state_id(fluid); - if new_state_id != current_block_state.id { - updated_fluid_state = new_fluid_state; - world - .set_block_state(block_pos, new_state_id, BlockFlags::NOTIFY_ALL) - .await; - // Schedule another tick to continue the flow/drain process - let tick_delay = self.get_flow_speed(world); - world - .schedule_fluid_tick( - fluid, - *block_pos, - tick_delay, - TickPriority::Normal, - ) - .await; - } - } else if self.is_waterlogged(world, block_pos).await.is_none() { - // Fluid should disappear completely - world - .set_block_state( - block_pos, - Block::AIR.default_state.id, - BlockFlags::NOTIFY_ALL, - ) - .await; - return; // No more flow to try - } - } - self.try_flow(world, fluid, block_pos, &updated_fluid_state) - .await; - }) - } - - fn try_flow<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - props: &'a FlowingFluidProperties, - ) -> FluidFuture<'a, ()> { - Box::pin(async move { - let below_pos = block_pos.down(); - - if self.can_replace_block(world, &below_pos, fluid).await { - let mut new_props = FlowingFluidProperties::default(fluid); - new_props.level = Level::L8; - new_props.falling = Falling::True; - - self.spread_to(world, fluid, &below_pos, new_props.to_state_id(fluid)) - .await; - if self - .count_neighboring_sources(world, fluid, block_pos) - .await - >= 3 - { - self.flow_to_sides(world, fluid, block_pos).await; - } - } else if props.level == Level::L8 && props.falling == Falling::False - || !self - .is_water_hole(world, fluid, block_pos, &below_pos) - .await - { - self.flow_to_sides(world, fluid, block_pos).await; - } - }) - } - - fn count_neighboring_sources<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - ) -> FluidFuture<'a, i32> { - Box::pin(async move { - let mut source_count = 0; - - for direction in BlockDirection::horizontal() { - let neighbor_pos = block_pos.offset(direction.to_offset()); - let neighbor_state_id = world.get_block_state_id(&neighbor_pos).await; - - if fluid.default_state_index == Fluid::WATER.default_state_index - && self.is_waterlogged(world, &neighbor_pos).await.is_some() - { - source_count += 1; - continue; - } - - if !self.is_same_fluid(fluid, neighbor_state_id) { - continue; - } - - let neighbor_props = - FlowingFluidProperties::from_state_id(neighbor_state_id, fluid); - let neighbor_level = i32::from(neighbor_props.level.to_index()) + 1; - - if neighbor_level == 8 && neighbor_props.falling != Falling::True { - source_count += 1; - } - } - source_count - }) - } - - fn get_new_liquid<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - ) -> FluidFuture<'a, Option> { - Box::pin(async move { - let current_state_id = world.get_block_state_id(block_pos).await; - - let current_props = FlowingFluidProperties::from_state_id(current_state_id, fluid); - let current_level = i32::from(current_props.level.to_index()) + 1; - if current_level == 8 && current_props.falling != Falling::True { - return Some(current_props); - } - let mut highest_level = 0; - let mut source_count = 0; - - for direction in BlockDirection::horizontal() { - let neighbor_pos = block_pos.offset(direction.to_offset()); - let neighbor_state_id = world.get_block_state_id(&neighbor_pos).await; - - if fluid.default_state_index == Fluid::WATER.default_state_index - && self.is_waterlogged(world, &neighbor_pos).await.is_some() - { - source_count += 1; - highest_level = highest_level.max(8); - continue; - } - - if !self.is_same_fluid(fluid, neighbor_state_id) { - continue; - } - - let neighbor_props = - FlowingFluidProperties::from_state_id(neighbor_state_id, fluid); - let neighbor_level = i32::from(neighbor_props.level.to_index()) + 1; - - if neighbor_level == 8 && neighbor_props.falling != Falling::True { - source_count += 1; - } - - highest_level = highest_level.max(neighbor_level); - } - - if source_count >= 2 && self.can_convert_to_source(world) { - let below_pos = block_pos.down(); - let below_state_id = world.get_block_state_id(&below_pos).await; - if self - .can_flow_through(world, &below_pos, below_state_id, fluid) - .await - { - return Some(self.get_source(fluid, false).await); - } - } - - let above_pos = block_pos.up(); - let above_state_id = world.get_block_state_id(&above_pos).await; - - if self.is_same_fluid(fluid, above_state_id) - || self.is_waterlogged(world, &above_pos).await.is_some() - { - return Some(self.get_flowing(fluid, Level::L8, true).await); - } - - let drop_off = self.get_level_decrease_per_block(world); - let new_level = highest_level - drop_off; - - if new_level <= 0 { - return None; - } - if new_level != current_level { - return Some( - self.get_flowing(fluid, Level::from_index(new_level as u16 - 1), false) - .await, - ); - } - Some(current_props) - }) - } - - fn can_flow_through<'a>( - &'a self, - world: &'a Arc, - block_pos: &'a BlockPos, - state_id: BlockStateId, - fluid: &'a Fluid, - ) -> FluidFuture<'a, bool> { - Box::pin(async move { - if self.is_same_fluid(fluid, state_id) { - let props = FlowingFluidProperties::from_state_id(state_id, fluid); - if props.level == Level::L8 && props.falling != Falling::True { - return true; - } - } - let state = world.get_block_state(block_pos).await; - - // If the block has a solid top face, water can flow horizontally over it. - // This allows water to spread across the tops of solid blocks like stone, dirt, etc. - if state.is_side_solid(BlockDirection::Up) { - return true; - } - - // Also allow flow through replaceable blocks (like tall grass) but not - // through non-replaceable non-solid blocks such as doors/fences. - state.replaceable() - }) - } - - fn flow_to_sides<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - ) -> FluidFuture<'a, ()> { - Box::pin(async move { - let block_state_id = world.get_block_state_id(block_pos).await; - - let is_fluid_state_id = fluid - .states - .iter() - .any(|state| state.block_state_id == block_state_id); - if !is_fluid_state_id { - return; - } - - let props = FlowingFluidProperties::from_state_id(block_state_id, fluid); - let drop_off = self.get_level_decrease_per_block(world); - - let level = i32::from(props.level.to_index()) - drop_off; - - let effective_level = if props.falling == Falling::True { - 7 - } else { - level - }; - if effective_level <= 0 { - return; - } - - let spread_dirs = self.get_spread(world, fluid, block_pos).await; - - for (direction, _slope_dist) in spread_dirs { - let side_pos = block_pos.offset(direction.to_offset()); - - if self.can_replace_block(world, &side_pos, fluid).await { - let new_props = self - .get_flowing(fluid, Level::from_index(effective_level as u16 - 1), false) - .await; - self.spread_to(world, fluid, &side_pos, new_props.to_state_id(fluid)) - .await; - } - } - }) - } - - fn get_spread<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: &'a BlockPos, - ) -> FluidFuture<'a, HashMap> { - Box::pin(async move { - let mut min_dist = 1000; - let mut result = HashMap::new(); - let mut ctx = None; - for direction in BlockDirection::horizontal() { - let side_pos = block_pos.offset(direction.to_offset()); - let side_state_id = world.get_block_state_id(&side_pos).await; - - let side_props = FlowingFluidProperties::from_state_id(side_state_id, fluid); - - if !self.can_pass_through(world, fluid, &side_pos).await - || (side_props.level == Level::L8 && side_props.falling != Falling::True) - { - continue; - } - - if ctx.is_none() { - ctx = Some(SpreadContext::new()); - } - - let ctx_ref = ctx.as_mut().unwrap(); - - let slope_dist = if ctx_ref.is_hole(self, world, fluid, &side_pos).await { - 0 - } else { - self.get_in_flow_down_distance( - world, - fluid, - side_pos, - 1, - direction.opposite(), - ctx_ref, - ) - .await - }; - - if slope_dist < min_dist { - result.clear(); - } - - if slope_dist <= min_dist { - result.insert(direction, slope_dist); - min_dist = slope_dist; - } - } - result - }) - } - - fn get_in_flow_down_distance<'a, 'b>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - block_pos: BlockPos, - distance: i32, - exclude_dir: BlockDirection, - ctx: &'b mut SpreadContext, - ) -> BlockFuture<'b, i32> - where - 'a: 'b, - { - Box::pin(async move { - if distance > self.get_max_flow_distance(world) { - return 1000; - } - - let mut min_dist = 1000; - - for direction in BlockDirection::horizontal() { - if direction == exclude_dir { - continue; - } - - let next_pos = block_pos.offset(direction.to_offset()); - - if !self.can_pass_through(world, fluid, &next_pos).await { - continue; - } - - let next_state_id = world.get_block_state_id(&next_pos).await; - - if self.is_same_fluid(fluid, next_state_id) { - let next_props = FlowingFluidProperties::from_state_id(next_state_id, fluid); - if next_props.level == Level::L8 && next_props.falling == Falling::False { - return 1000; - } - } - - if ctx.is_hole(self, world, fluid, &next_pos).await { - return distance; - } - - let next_dist = self - .get_in_flow_down_distance( - world, - fluid, - next_pos, - distance + 1, - direction.opposite(), - ctx, - ) - .await; - - min_dist = min_dist.min(next_dist); - } - min_dist - }) - } - - fn spread_to<'a>( - &'a self, - world: &'a Arc, - _fluid: &'a Fluid, - pos: &'a BlockPos, - state_id: BlockStateId, - ) -> FluidFuture<'a, ()> { - Box::pin(async move { - // If the target block already has waterlogged=true, don't change it. - if self.is_waterlogged(world, pos).await.is_some() { - return; - } - - // If the block at `pos` has a `waterlogged` property, set it to true - // instead of replacing the whole block with a fluid block state. - let block = world.get_block(pos).await; - let current_state_id = world.get_block_state_id(pos).await; - - // Check if the block should be broken to drop loot - if block.id != Block::AIR.id { - world.break_block(pos, None, BlockFlags::NOTIFY_ALL).await; - } - - // Extract the new state before any await to avoid Send issues - let new_waterlogged_state = { - block.properties(current_state_id).and_then(|properties| { - let original_props = properties.to_props(); - // If the block has a waterlogged property, set it to "true". - original_props - .iter() - .any(|(k, _)| *k == "waterlogged") - .then(|| { - let props: Vec<(&str, &str)> = original_props - .iter() - .map(|(key, value)| { - if *key == "waterlogged" { - ("waterlogged", "true") - } else { - (*key, *value) - } - }) - .collect(); - block.from_properties(&props).to_state_id(block) - }) - }) - }; - - if let Some(new_state) = new_waterlogged_state { - world - .set_block_state(pos, new_state, BlockFlags::NOTIFY_ALL) - .await; - return; - } - - // Default: replace the block with the fluid state. - world - .set_block_state(pos, state_id, BlockFlags::NOTIFY_ALL) - .await; - }) - } - - fn can_pass_through<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - pos: &'a BlockPos, - ) -> BlockFuture<'a, bool> { - Box::pin(async move { - let state_id = world.get_block_state_id(pos).await; - - if self.is_same_fluid(fluid, state_id) { - return true; - } - - self.can_replace_block(world, pos, fluid).await - }) - } - - fn can_replace_block<'a>( - &'a self, - world: &'a Arc, - pos: &'a BlockPos, - fluid: &'a Fluid, - ) -> BlockFuture<'a, bool> { - Box::pin(async move { - let block = world.get_block(pos).await; - self.can_be_replaced(world, pos, block.id, fluid).await - }) - } - - fn can_be_replaced<'a>( - &'a self, - world: &'a Arc, - pos: &'a BlockPos, - block_id: BlockId, - fluid: &'a Fluid, - ) -> BlockFuture<'a, bool> { - Box::pin(async move { - // let block_state_id = world.get_block_state_id(pos).await; - let block_state = world.get_block_state(pos).await; - let block = Block::from_id(block_id); - - if let Some(other_fluid) = Fluid::from_state_id(block_state.id) { - if fluid.id != other_fluid.id { - return true; - } - if other_fluid.is_source(block_state.id) && other_fluid.is_falling(block_state.id) { - return true; - } - } - - // Even if these are PistonBehavior::Destroy, water shouldn't replace them - if block.has_tag(&tag::Block::MINECRAFT_DOORS) - || block.has_tag(&tag::Block::MINECRAFT_BEDS) - { - return false; - } - - // Only allow replacing if the current block state is marked replaceable - // (e.g. tall grass) or the block is air. This prevents non-replaceable, - // non-solid blocks from being overwritten by fluids. - block_state.replaceable() - || block_id == Block::AIR.id - || block_state.piston_behavior == pumpkin_data::block_state::PistonBehavior::Destroy - }) - } - - fn is_water_hole<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - _pos: &'a BlockPos, - below_pos: &'a BlockPos, - ) -> BlockFuture<'a, bool> { - Box::pin(async move { - let below_state_id = world.get_block_state_id(below_pos).await; - - if self.is_same_fluid(fluid, below_state_id) { - return true; - } - - if self.can_replace_block(world, below_pos, fluid).await { - return true; - } - - false - }) - } -} diff --git a/pumpkin/src/block/fluid/flowing_trait.rs b/pumpkin/src/block/fluid/flowing_trait.rs new file mode 100644 index 000000000..fe48e99ee --- /dev/null +++ b/pumpkin/src/block/fluid/flowing_trait.rs @@ -0,0 +1,547 @@ +use super::{pathfinder, physics}; +use crate::{block::BlockFuture, world::World}; +use pumpkin_data::{ + Block, BlockDirection, + fluid::{EnumVariants, Falling, Fluid, FluidProperties, Level}, +}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::{BlockStateId, tick::TickPriority, world::BlockFlags}; +use std::sync::Arc; +pub type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; +pub type FluidFuture<'a, T> = BlockFuture<'a, T>; + +pub trait FlowingFluid: Send + Sync { + fn get_level_decrease_per_block(&self, world: &World) -> i32; + fn get_flow_speed(&self, world: &World) -> u8; + + fn get_source(&self, fluid: &Fluid, falling: bool) -> FlowingFluidProperties { + let mut source_props = FlowingFluidProperties::default(fluid); + source_props.level = Level::L8; + source_props.falling = if falling { + Falling::True + } else { + Falling::False + }; + source_props + } + + fn get_flowing(&self, fluid: &Fluid, level: Level, falling: bool) -> FlowingFluidProperties { + let mut flowing_props = FlowingFluidProperties::default(fluid); + flowing_props.level = level; + flowing_props.falling = if falling { + Falling::True + } else { + Falling::False + }; + flowing_props + } + + fn get_max_flow_distance(&self, world: &World) -> i32; + fn can_convert_to_source(&self, world: &Arc) -> bool; + + fn is_same_fluid(&self, fluid: &Fluid, other_state_id: BlockStateId) -> bool { + Fluid::from_state_id(other_state_id).is_some_and(|other| fluid.id == other.id) + } + + /// Core fluid tick handler that updates fluid state and triggers spreading. + /// + /// Processes scheduled fluid ticks by: + /// 1. Validating the block contains fluid + /// 2. Updating non-source fluid levels based on neighbors + /// 3. Triggering fluid spread to adjacent positions + /// + /// Sources (level 8, non-falling) always spread without state changes. + fn on_scheduled_tick_internal<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + block_pos: &'a BlockPos, + ) -> impl std::future::Future + Send + 'a { + async move { + //let block = world.get_block(block_pos).await; + let current_block_state_id = world.get_block_state_id(block_pos).await; + let block = Block::from_state_id(current_block_state_id); + + let waterlogged = block.is_waterlogged(current_block_state_id); + let is_fluid_state_id = fluid + .states + .iter() + .any(|state| state.block_state_id == current_block_state_id) + || waterlogged; + + if !is_fluid_state_id { + return; + } + + let current_fluid_state = + FlowingFluidProperties::from_state_id(current_block_state_id, fluid); + let is_source = current_fluid_state.level == Level::L8 + && current_fluid_state.falling != Falling::True; + let state_for_spreading: FlowingFluidProperties; + + // Update state if non-source + if !is_source && !waterlogged { + let new_fluid_state = self.get_new_liquid(world, fluid, block_pos).await; + + if let Some(new_state) = new_fluid_state { + let new_state_id = new_state.to_state_id(fluid); + + if new_state_id != current_block_state_id { + world + .set_block_state(block_pos, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + + // Schedule next tick for this position + let tick_delay = self.get_flow_speed(world); + world + .schedule_fluid_tick( + fluid, + *block_pos, + tick_delay, + TickPriority::Normal, + ) + .await; + } + + // Use the new state for spreading + state_for_spreading = new_state; + } else { + if !waterlogged { + world + .set_block_state( + block_pos, + Block::AIR.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + return; // Don't spread if fluid is gone + } + } else { + // Sources use their current state + state_for_spreading = current_fluid_state; + } + + // Then, spread using the appropriate state + self.try_flow(world, fluid, block_pos, &state_for_spreading) + .await; + } + } + + /// Attempts to flow fluid from a position, prioritizing downward flow. + /// + /// Flow priority: + /// 1. Down - if space below, create falling fluid (level 8) + /// 2. Sides - spread horizontally using pathfinding + /// + /// Sources with 3+ adjacent sources also spread to sides when flowing down. + fn try_flow<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + block_pos: &'a BlockPos, + props: &'a FlowingFluidProperties, + ) -> impl std::future::Future + Send + 'a { + async move { + let below_pos = block_pos.down(); + let below_state = world.get_block_state(&below_pos).await; + let below_block = Block::from_state_id(below_state.id); + let is_hole = physics::can_be_replaced(below_state, below_block, fluid); + + // Try to flow down first + if is_hole { + let falling_props = self.get_flowing(fluid, Level::L8, true); + self.spread_to(world, fluid, &below_pos, falling_props.to_state_id(fluid)) + .await; + + // Check if we should also spread to sides + if props.level == Level::L8 && props.falling == Falling::False { + let source_count = self.count_source_neighbors(world, fluid, block_pos).await; + if source_count >= 3 { + self.flow_to_sides(world, fluid, block_pos).await; + } + } + return; + } + + // Check if fluid should flow to the side(s) + self.flow_to_sides(world, fluid, block_pos).await; + } + } + + fn count_source_neighbors<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + block_pos: &'a BlockPos, + ) -> impl std::future::Future + Send + 'a { + async move { + let mut count = 0; + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let neighbor_pos = block_pos.offset(direction.to_offset()); + let neighbor_id = world.get_block_state_id(&neighbor_pos).await; + if self.is_same_fluid(fluid, neighbor_id) { + let props = FlowingFluidProperties::from_state_id(neighbor_id, fluid); + if props.level == Level::L8 && props.falling == Falling::False { + count += 1; + } + } + } + count + } + } + + /// Calculates the new fluid state for a position based on neighbors and environment. + /// + /// Priority order: + /// 1. Sources remain unchanged + /// 2. Infinite source formation (2+ adjacent sources + solid/source below) + /// 3. Fluid above forces falling state (level 8, falling) + /// 4. Standard flow calculation from highest neighbor minus dropoff + /// + /// # Returns + /// New fluid properties, or None if fluid should drain + fn get_new_liquid<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + block_pos: &'a BlockPos, + ) -> impl std::future::Future> + Send + 'a { + async move { + let current_state_id = world.get_block_state_id(block_pos).await; + let current_props = FlowingFluidProperties::from_state_id(current_state_id, fluid); + + // Sources never change + if current_props.level == Level::L8 && current_props.falling != Falling::True { + return Some(current_props); + } + + // First: check horizontal neighbors for infinite source formation + let mut highest_neighbor = 0; + let mut neighbor_source_count = 0; + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let neighbor_pos = block_pos.offset(direction.to_offset()); + let neighbor_state_id = world.get_block_state_id(&neighbor_pos).await; + if !self.is_same_fluid(fluid, neighbor_state_id) { + continue; + } + + let neighbor_props = + FlowingFluidProperties::from_state_id(neighbor_state_id, fluid); + + // Count horizontal non-falling sources for infinite source formation + if neighbor_props.level == Level::L8 && neighbor_props.falling == Falling::False { + neighbor_source_count += 1; + } + + // Falling water from the side counts as level 8 + let neighbor_level = if neighbor_props.falling == Falling::True { + 8 + } else { + i32::from(neighbor_props.level.to_index()) + 1 + }; + + highest_neighbor = highest_neighbor.max(neighbor_level); + } + + // Attempt infinite source formation first + if self.can_convert_to_source(world) && neighbor_source_count >= 2 { + let below_pos = block_pos.down(); + let below_state = world.get_block_state(&below_pos).await; + let below_state_id = below_state.id; + + // Check if block below is a stable source of the same fluid + let below_is_same_source = if self.is_same_fluid(fluid, below_state_id) { + let below_props = FlowingFluidProperties::from_state_id(below_state_id, fluid); + below_props.level == Level::L8 && below_props.falling == Falling::False + } else { + false + }; + + // If the block below is solid (solid block) or a source of same fluid, form a source here. + if below_is_same_source || below_state.is_solid_block() { + return Some(self.get_source(fluid, false)); + } + // Otherwise continue to standard falling/flowing logic + } + + // Then: if there's water above, this block is ALWAYS level 8, falling=true + let above_pos = block_pos.up(); + let above_state_id = world.get_block_state_id(&above_pos).await; + let above_block = Block::from_state_id(above_state_id); + + if self.is_same_fluid(fluid, above_state_id) + || above_block.is_waterlogged(above_state_id) + { + return Some(self.get_flowing(fluid, Level::L8, true)); + } + + // Standard flowing calculation + let drop_off = self.get_level_decrease_per_block(world); + let new_level = highest_neighbor - drop_off; + + if new_level <= 0 { + None + } else { + Some(self.get_flowing(fluid, Level::from_index(new_level as u16 - 1), false)) + } + } + } + + /// Core spread logic with quiescence checks and state updates. + /// + /// Implements: + /// - Quiescence: prevents unnecessary updates (e.g., source blocks, lower levels) + /// - Infinite source formation checks (before and after placement) + /// - Block replacement for non-fluid blocks + /// - Fluid tick scheduling for non-source blocks + /// + /// Called by `spread_to` implementations after fluid-specific pre-checks. + fn apply_spread<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + pos: &'a BlockPos, + state_id: BlockStateId, + new_props: FlowingFluidProperties, + ) -> impl std::future::Future + Send + 'a { + async move { + let current_state_id = world.get_block_state_id(pos).await; + let is_already_same_fluid = self.is_same_fluid(fluid, current_state_id); + if is_already_same_fluid { + let current_props = FlowingFluidProperties::from_state_id(current_state_id, fluid); + + let current_level = i32::from(current_props.level.to_index()) + 1; + let new_level = i32::from(new_props.level.to_index()) + 1; + let current_is_source = + current_props.level == Level::L8 && current_props.falling == Falling::False; + let new_is_source = + new_props.level == Level::L8 && new_props.falling == Falling::False; + + // Never overwrite a source with anything + if current_is_source { + return; + } + + // Check for infinite source formation before quiescence checks + if !current_is_source && self.can_convert_to_source(world) { + let should_convert = self + .check_infinite_source_formation(world, fluid, pos) + .await; + + if should_convert { + let source_props = self.get_source(fluid, false); + let source_state_id = source_props.to_state_id(fluid); + world + .set_block_state(pos, source_state_id, BlockFlags::NOTIFY_ALL) + .await; + + // Sources don't need ticks + return; + } + } + + // If new is a source, always accept it + if new_is_source { + + // Continue to set state below + } else if current_props.falling == Falling::True + && new_props.falling == Falling::False + { + // Never downgrade falling to non-falling (unless new is a source, already checked above) + return; + } else if current_props.falling == new_props.falling { + // Same falling state - check level + if new_level <= current_level { + return; + } + } + } else { + // Replace non-fluid blocks + let block = world.get_block(pos).await; + if block.id != Block::AIR.id { + world.break_block(pos, None, BlockFlags::NOTIFY_ALL).await; + } + } + + world + .set_block_state(pos, state_id, BlockFlags::NOTIFY_ALL) + .await; + + // Check for infinite source formation after placing new fluid + if self.can_convert_to_source(world) { + let should_convert = self + .check_infinite_source_formation(world, fluid, pos) + .await; + + if should_convert { + let source_props = self.get_source(fluid, false); + let source_state_id = source_props.to_state_id(fluid); + world + .set_block_state(pos, source_state_id, BlockFlags::NOTIFY_ALL) + .await; + + // Sources don't need ticks + return; + } + } + + // Only schedule tick if not a source + let is_source = new_props.level == Level::L8 && new_props.falling == Falling::False; + + if !is_source { + let tick_delay = self.get_flow_speed(world); + world + .schedule_fluid_tick(fluid, *pos, tick_delay, TickPriority::Normal) + .await; + } + } + } + + /// Checks if infinite source formation conditions are met. + /// + /// Requirements: + /// - 2+ horizontally adjacent source blocks (level 8, non-falling) + /// - Block below is either solid OR a source of the same fluid + /// + /// # Returns + /// `true` if position should convert to a source block + fn check_infinite_source_formation<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + pos: &'a BlockPos, + ) -> impl std::future::Future + Send + 'a { + async move { + // Count adjacent horizontal source blocks + let mut source_count = 0; + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let neighbor_pos = pos.offset(direction.to_offset()); + let neighbor_state_id = world.get_block_state_id(&neighbor_pos).await; + + if self.is_same_fluid(fluid, neighbor_state_id) { + let neighbor_props = + FlowingFluidProperties::from_state_id(neighbor_state_id, fluid); + + if neighbor_props.level == Level::L8 && neighbor_props.falling == Falling::False + { + source_count += 1; + } + } + } + + // Need at least 2 source neighbors + if source_count < 2 { + return false; + } + + // Check the block below + let below_pos = pos.down(); + let below_state = world.get_block_state(&below_pos).await; + let below_state_id = below_state.id; + + // Check if block below is a stable source of the same fluid + let below_is_same_source = if self.is_same_fluid(fluid, below_state_id) { + let below_props = FlowingFluidProperties::from_state_id(below_state_id, fluid); + + below_props.level == Level::L8 && below_props.falling == Falling::False + } else { + false + }; + + // Convert to source if below is solid or a source of same fluid + below_is_same_source || below_state.is_solid_block() + } + } + + /// Spreads fluid to a target position with the given state. + /// + /// Default implementation delegates to `apply_spread`. Implementations like + /// lava can override to add fluid-specific logic (e.g., water -> stone conversion). + fn spread_to<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + pos: &'a BlockPos, + state_id: BlockStateId, + ) -> impl std::future::Future + Send + 'a { + async move { + let new_props = FlowingFluidProperties::from_state_id(state_id, fluid); + self.apply_spread(world, fluid, pos, state_id, new_props) + .await; + } + } + + /// Spreads fluid horizontally to adjacent positions using pathfinding. + /// + /// Uses `get_spread` to find optimal flow directions (shortest distance to holes). + /// Decreases fluid level by dropoff amount when flowing to sides. + /// Falling fluids maintain higher levels when spreading horizontally. + fn flow_to_sides<'a>( + &'a self, + world: &'a Arc, + fluid: &'a Fluid, + block_pos: &'a BlockPos, + ) -> impl std::future::Future + Send + 'a { + async move { + let block_state_id = world.get_block_state_id(block_pos).await; + let props = FlowingFluidProperties::from_state_id(block_state_id, fluid); + let drop_off = self.get_level_decrease_per_block(world); + let current_level = i32::from(props.level.to_index()) + 1; + let effective_level = if props.falling == Falling::True { + (8 - drop_off).min(8) + } else { + current_level - drop_off + }; + + if effective_level <= 0 { + return; + } + + let (spread_dirs, count) = pathfinder::get_spread(self, world, fluid, block_pos).await; + let mut min_dist = i32::MAX; + for (_, dist) in spread_dirs.iter().take(count) { + min_dist = min_dist.min(*dist); + } + + for (direction, slope_dist) in spread_dirs.iter().take(count).copied() { + if slope_dist != min_dist { + continue; + } + let side_pos = block_pos.offset(direction.to_offset()); + + // Re-verify the target position is still replaceable right before spreading + let side_state = world.get_block_state(&side_pos).await; + let side_block = Block::from_state_id(side_state.id); + + if !physics::can_be_replaced(side_state, side_block, fluid) { + continue; + } + + // Calculate the new fluid state + let level_index = (effective_level as u16).saturating_sub(1); + let new_props = self.get_flowing(fluid, Level::from_index(level_index), false); + let final_state_id = new_props.to_state_id(fluid); + + // Call spread_to with the calculated fluid state ID + self.spread_to(world, fluid, &side_pos, final_state_id) + .await; + } + } + } +} diff --git a/pumpkin/src/block/fluid/lava.rs b/pumpkin/src/block/fluid/lava.rs index 3068c11be..2480231ff 100644 --- a/pumpkin/src/block/fluid/lava.rs +++ b/pumpkin/src/block/fluid/lava.rs @@ -1,5 +1,9 @@ -use std::sync::Arc; - +use super::flowing_trait::FlowingFluid; +use crate::{ + block::{BlockFuture, BlockMetadata, fluid::FluidBehaviour}, + entity::EntityBase, + world::World, +}; use pumpkin_data::{ Block, BlockDirection, dimension::Dimension, @@ -8,17 +12,7 @@ use pumpkin_data::{ }; use pumpkin_util::math::position::BlockPos; use pumpkin_world::{BlockStateId, tick::TickPriority, world::BlockFlags}; - -use crate::{ - block::{ - BlockFuture, BlockMetadata, - fluid::{FluidBehaviour, flowing::FluidFuture}, - }, - entity::EntityBase, - world::World, -}; - -use super::flowing::FlowingFluid; +use std::sync::Arc; type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; pub struct FlowingLava; @@ -36,7 +30,7 @@ impl FlowingLava { _fluid: &Fluid, block_pos: &BlockPos, ) -> bool { - // Logic to determine if we should replace the fluid with any of (cobble, obsidian, stone or basalt) + // Logic to determine if we should replace the fluid with any of (cobble, obsidian, stone, etc.) let below_is_soul_soil = world .get_block(&block_pos.offset(BlockDirection::Down.to_offset())) .await @@ -147,7 +141,7 @@ impl FluidBehaviour for FlowingLava { impl FlowingFluid for FlowingLava { fn get_level_decrease_per_block(&self, world: &World) -> i32 { - // ultrawarm logic + // Ultrawarm logic if world.dimension == Dimension::THE_NETHER { 1 } else { @@ -156,7 +150,7 @@ impl FlowingFluid for FlowingLava { } fn get_flow_speed(&self, world: &World) -> u8 { - // ultrawarm logic - lava flows faster in the Nether + // Ultrawarm logic - lava flows faster in the Nether if world.dimension == Dimension::THE_NETHER { LAVA_FLOW_SPEED_NETHER } else { @@ -165,50 +159,50 @@ impl FlowingFluid for FlowingLava { } fn get_max_flow_distance(&self, world: &World) -> i32 { - // ultrawarm logic + // Ultrawarm logic if world.dimension == Dimension::THE_NETHER { - 4 + 5 } else { - 2 + 3 } } fn can_convert_to_source(&self, _world: &Arc) -> bool { - //TODO add game rule check for lava conversion + // TODO: add game rule check for lava conversion false } - fn spread_to<'a>( - &'a self, - world: &'a Arc, - fluid: &'a Fluid, - pos: &'a BlockPos, + async fn spread_to( + &self, + world: &Arc, + fluid: &Fluid, + pos: &BlockPos, state_id: BlockStateId, - ) -> FluidFuture<'a, ()> { - Box::pin(async move { - let mut new_props = FlowingFluidProperties::default(fluid); - new_props.level = Level::L8; - new_props.falling = Falling::True; - if state_id == new_props.to_state_id(fluid) { - // STONE creation - if world.get_block(pos).await == &Block::WATER { - world - .set_block_state(pos, Block::STONE.default_state.id, BlockFlags::NOTIFY_ALL) - .await; - world - .sync_world_event(WorldEvent::LavaExtinguished, *pos, 0) - .await; - return; - } - } + ) { + let new_props = FlowingFluidProperties::from_state_id(state_id, fluid); + let current_state_id = world.get_block_state_id(pos).await; + let block = Block::from_state_id(current_state_id); - if self.is_waterlogged(world, pos).await.is_some() { + if new_props.level == Level::L8 && new_props.falling == Falling::True { + // Stone creation when lava meets water + if block == &Block::WATER { + world + .set_block_state(pos, Block::STONE.default_state.id, BlockFlags::NOTIFY_ALL) + .await; + world + .sync_world_event(WorldEvent::LavaExtinguished, *pos, 0) + .await; return; } + } - world - .set_block_state(pos, state_id, BlockFlags::NOTIFY_ALL) - .await; - }) + // Don't flow into waterlogged blocks + if block.is_waterlogged(current_state_id) { + return; + } + + // Delegate quiescence, replacement and scheduling to the shared helper + self.apply_spread(world, fluid, pos, state_id, new_props) + .await; } } diff --git a/pumpkin/src/block/fluid/mod.rs b/pumpkin/src/block/fluid/mod.rs index 31fc5fd11..447b4ee09 100644 --- a/pumpkin/src/block/fluid/mod.rs +++ b/pumpkin/src/block/fluid/mod.rs @@ -1,20 +1,26 @@ -pub mod flowing; +pub mod flowing_trait; pub mod lava; +pub mod pathfinder; +pub mod physics; pub mod water; -use std::sync::Arc; +// Re-export for backward compatibility +pub mod flowing { + pub use super::flowing_trait::*; + pub use super::pathfinder::*; + pub use super::physics::*; +} +use super::{BlockIsReplacing, registry::BlockActionResult}; use crate::block::BlockFuture; use crate::entity::{EntityBase, player::Player}; +use crate::{server::Server, world::World}; use pumpkin_data::BlockDirection; use pumpkin_data::{fluid::Fluid, item::Item}; use pumpkin_protocol::java::server::play::SUseItemOn; use pumpkin_util::math::position::BlockPos; use pumpkin_world::BlockStateId; - -use crate::{server::Server, world::World}; - -use super::{BlockIsReplacing, registry::BlockActionResult}; +use std::sync::Arc; pub trait FluidBehaviour: Send + Sync { fn normal_use<'a>( diff --git a/pumpkin/src/block/fluid/pathfinder.rs b/pumpkin/src/block/fluid/pathfinder.rs new file mode 100644 index 000000000..05d671dee --- /dev/null +++ b/pumpkin/src/block/fluid/pathfinder.rs @@ -0,0 +1,225 @@ +use crate::block::fluid::flowing_trait::FlowingFluid; +use crate::world::World; +use pumpkin_data::{ + Block, BlockDirection, + fluid::{EnumVariants, Falling, Fluid, FluidProperties, Level}, +}; +use pumpkin_util::math::position::BlockPos; +use std::sync::Arc; +type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; +use super::physics; + +/// Represents a node in the BFS pathfinding queue for fluid flow calculation. +#[derive(Clone, Copy)] +pub struct PathNode { + pub pos: BlockPos, + pub distance: i32, + pub exclude_dir: BlockDirection, +} + +/// Checks if a position has a hole (downward flow opportunity) below it. +async fn is_hole(world: &Arc, fluid: &Fluid, pos: &BlockPos) -> bool { + let below_pos = pos.down(); + let below_state = world.get_block_state(&below_pos).await; + let below_block = Block::from_state_id(below_state.id); + physics::can_be_replaced(below_state, below_block, fluid) +} + +/// Determines valid spread directions for fluid flow using hole-first priority. +/// +/// - Holes (downward flow opportunities) get distance 0 priority +/// - All directions with equal minimum distance are returned +/// - Returns up to 4 directions in a fixed array with count +/// +/// # Returns +/// Tuple of (directions array, valid direction count) +pub async fn get_spread( + fluid_impl: &T, + world: &Arc, + fluid: &Fluid, + block_pos: &BlockPos, +) -> ([(BlockDirection, i32); 4], usize) { + let mut min_dist = 1000; + let mut result = [(BlockDirection::North, 1000); 4]; + let mut result_count = 0; + + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let side_pos = block_pos.offset(direction.to_offset()); + let side_state = world.get_block_state(&side_pos).await; + let side_state_id = side_state.id; + let side_block = Block::from_state_id(side_state.id); + + let side_props = FlowingFluidProperties::from_state_id(side_state_id, fluid); + + // Check if we can pass through (not a solid source block) + if !physics::can_be_replaced(side_state, side_block, fluid) + || (side_props.level == Level::L8 && side_props.falling != Falling::True) + { + continue; + } + + // Calculate what the new fluid state would be + let new_fluid_state = fluid_impl.get_new_liquid(world, fluid, &side_pos).await; + + // Skip if we can't actually place fluid here + if new_fluid_state.is_none() { + continue; + } + + let new_fluid_props = new_fluid_state.unwrap(); + + // Hole-first priority: holes get distance 0 + let slope_dist = if is_hole(world, fluid, &side_pos).await { + 0 + } else { + get_in_flow_down_distance_iterative( + fluid_impl, + world, + fluid, + side_pos, + direction.opposite(), + ) + .await + }; + + // Clear results if we find a shorter path + if slope_dist < min_dist { + result_count = 0; + min_dist = slope_dist; + } + + // Add all directions with equal minimum distance + if slope_dist <= min_dist { + // Check if the fluid at this position can be replaced + let can_replace = if fluid_impl.is_same_fluid(fluid, side_state_id) { + // Can replace if new level is higher or if target is falling + let target_level = i32::from(side_props.level.to_index()) + 1; + let new_level = i32::from(new_fluid_props.level.to_index()) + 1; + new_level > target_level || side_props.falling == Falling::True + } else { + // Can replace non-fluid blocks (already checked in can_be_replaced) + true + }; + + if can_replace && result_count < 4 { + result[result_count] = (direction, slope_dist); + result_count += 1; + } + } + } + (result, result_count) +} + +/// Performs iterative BFS search to find the shortest distance to a downward flow opportunity. +/// +/// Uses stack-allocated array for zero heap allocations. Searches up to `get_max_flow_distance` +/// (dynamic) horizontally from the starting position. +/// +/// # Returns +/// Distance to nearest hole, or 1000 if no hole found within search distance +pub async fn get_in_flow_down_distance_iterative( + fluid_impl: &T, + world: &Arc, + fluid: &Fluid, + start_pos: BlockPos, + initial_exclude_dir: BlockDirection, +) -> i32 { + const MAX_QUEUE_SIZE: usize = 64; + + let mut queue: [PathNode; MAX_QUEUE_SIZE] = [PathNode { + pos: BlockPos::new(0, 0, 0), + distance: 0, + exclude_dir: BlockDirection::North, + }; MAX_QUEUE_SIZE]; + + let mut queue_start = 0; + let mut queue_end = 0; + + queue[queue_end] = PathNode { + pos: start_pos, + distance: 1, + exclude_dir: initial_exclude_dir, + }; + queue_end = 1; + + let mut visited_bitset = [0u64; 4]; + let slope_find_distance = fluid_impl.get_max_flow_distance(world); + + let get_bit_index = |pos: BlockPos| -> Option { + let dx = pos.0.x - start_pos.0.x + slope_find_distance; + let dz = pos.0.z - start_pos.0.z + slope_find_distance; + let grid_size = slope_find_distance * 2 + 1; + (dx >= 0 && dx < grid_size && dz >= 0 && dz < grid_size) + .then(|| (dz * grid_size + dx) as usize) + }; + + while queue_start < queue_end { + let node = queue[queue_start]; + queue_start += 1; + + if node.distance > slope_find_distance { + continue; + } + + if let Some(bit_idx) = get_bit_index(node.pos) { + let word_idx = bit_idx / 64; + let bit_pos = bit_idx % 64; + if (visited_bitset[word_idx] & (1u64 << bit_pos)) != 0 { + continue; + } + visited_bitset[word_idx] |= 1u64 << bit_pos; + } + + // Check for hole (downward flow opportunity) + let below_pos = node.pos.down(); + let below_state = world.get_block_state(&below_pos).await; + let below_block = Block::from_state_id(below_state.id); + if physics::can_be_replaced(below_state, below_block, fluid) { + return node.distance; + } + + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + if direction == node.exclude_dir { + continue; + } + + let next_pos = node.pos.offset(direction.to_offset()); + + let next_state = world.get_block_state(&next_pos).await; + let next_block = Block::from_state_id(next_state.id); + if !physics::can_be_replaced(next_state, next_block, fluid) { + continue; + } + + // Source blocks block horizontal pathfinding + let next_state_id = world.get_block_state_id(&next_pos).await; + if fluid_impl.is_same_fluid(fluid, next_state_id) { + let next_props = FlowingFluidProperties::from_state_id(next_state_id, fluid); + if next_props.level == Level::L8 && next_props.falling == Falling::False { + continue; + } + } + + if queue_end < MAX_QUEUE_SIZE { + queue[queue_end] = PathNode { + pos: next_pos, + distance: node.distance + 1, + exclude_dir: direction.opposite(), + }; + queue_end += 1; + } + } + } + + 1000 +} diff --git a/pumpkin/src/block/fluid/physics.rs b/pumpkin/src/block/fluid/physics.rs new file mode 100644 index 000000000..5a0096597 --- /dev/null +++ b/pumpkin/src/block/fluid/physics.rs @@ -0,0 +1,51 @@ +use pumpkin_data::BlockState; +use pumpkin_data::tag::Taggable; +use pumpkin_data::{Block, fluid::Fluid, tag}; + +/// Check if a specific block can be replaced by fluid (based on block properties) +#[must_use] +pub fn can_be_replaced(block_state: &BlockState, block: &Block, fluid: &Fluid) -> bool { + // Fluid Logic + if let Some(other_fluid) = Fluid::from_state_id(block_state.id) { + if fluid.id != other_fluid.id { + return true; + } + // Replace current fluid if it is a falling source + if other_fluid.is_source(block_state.id) && other_fluid.is_falling(block_state.id) { + return true; + } + } + + let id = block.id; + + // Blocks that fluid should never replace + if block.has_tag(&tag::Block::MINECRAFT_DOORS) + || block.has_tag(&tag::Block::MINECRAFT_BEDS) + || block.has_tag(&tag::Block::MINECRAFT_LEAVES) + || block.has_tag(&tag::Block::MINECRAFT_PRESSURE_PLATES) + || block.has_tag(&tag::Block::C_CLUSTERS) + || block.has_tag(&tag::Block::MINECRAFT_WALL_CORALS) + || block.has_tag(&tag::Block::MINECRAFT_SHULKER_BOXES) + || block.has_tag(&tag::Block::MINECRAFT_PORTALS) + || id == Block::BELL.id + || id == Block::BIG_DRIPLEAF.id + || id == Block::BIG_DRIPLEAF_STEM.id + || id == Block::CAKE.id + || id == Block::CONDUIT.id + || id == Block::CAMPFIRE.id + || id == Block::DRAGON_EGG.id + || id == Block::KELP.id + || id == Block::LADDER.id + || id == Block::POINTED_DRIPSTONE.id + || id == Block::SCAFFOLDING.id + { + return false; + } + + // Only replace air, explicitly replaceable blocks, or carpets + block_state.replaceable() + || id == Block::AIR.id + || block.has_tag(&tag::Block::MINECRAFT_WOOL_CARPETS) + // Only use PistonBehavior::Destroy if it didn't pass the checks above + || block_state.piston_behavior == pumpkin_data::block_state::PistonBehavior::Destroy +} diff --git a/pumpkin/src/block/fluid/water.rs b/pumpkin/src/block/fluid/water.rs index 255293ea2..34cf14666 100644 --- a/pumpkin/src/block/fluid/water.rs +++ b/pumpkin/src/block/fluid/water.rs @@ -1,16 +1,13 @@ -use std::sync::Arc; - -use pumpkin_data::fluid::Fluid; -use pumpkin_util::math::position::BlockPos; -use pumpkin_world::{BlockStateId, tick::TickPriority}; - +use super::flowing_trait::FlowingFluid; use crate::{ block::{BlockFuture, BlockMetadata, fluid::FluidBehaviour}, entity::EntityBase, world::World, }; - -use super::flowing::FlowingFluid; +use pumpkin_data::fluid::Fluid; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::{BlockStateId, tick::TickPriority}; +use std::sync::Arc; pub struct FlowingWater; @@ -60,10 +57,13 @@ impl FluidBehaviour for FlowingWater { block_pos: &'a BlockPos, _notify: bool, ) -> BlockFuture<'a, ()> { - Box::pin(async { - world - .schedule_fluid_tick(fluid, *block_pos, WATER_FLOW_SPEED, TickPriority::Normal) - .await; + Box::pin(async move { + // Avoid rescheduling a fluid tick if one is already queued. + if !world.is_fluid_tick_scheduled(block_pos, fluid).await { + world + .schedule_fluid_tick(fluid, *block_pos, WATER_FLOW_SPEED, TickPriority::Normal) + .await; + } }) } @@ -84,11 +84,11 @@ impl FlowingFluid for FlowingWater { } fn get_max_flow_distance(&self, _world: &World) -> i32 { - 4 + 5 } fn can_convert_to_source(&self, _world: &Arc) -> bool { - //TODO add game rule check for water conversion + // TODO: add game rule check for water conversion true } }