chore: optimized, working fluids (#1392)

* fix: optimized, working fluids

* chore: clippy and formatting
This commit is contained in:
RB007
2026-01-29 10:53:00 -05:00
committed by GitHub
parent 54d99b8ae6
commit 4ec9db1e36
8 changed files with 902 additions and 747 deletions

View File

@@ -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;

View File

@@ -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<BlockPos, bool>,
}
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<T: FlowingFluid + ?Sized + Sync>(
&mut self,
fluid: &T,
world: &Arc<World>,
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<Box<dyn Future<Output = T> + 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<World>) -> bool;
fn is_waterlogged<'a>(
&'a self,
world: &'a Arc<World>,
pos: &'a BlockPos,
) -> FluidFuture<'a, Option<BlockStateId>> {
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<World>,
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<World>,
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<World>,
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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> FluidFuture<'a, Option<FlowingFluidProperties>> {
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<World>,
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<World>,
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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> FluidFuture<'a, HashMap<BlockDirection, i32>> {
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<World>,
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<World>,
_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<World>,
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<World>,
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<World>,
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<World>,
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
})
}
}

View File

@@ -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<World>) -> 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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> impl std::future::Future<Output = ()> + 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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
props: &'a FlowingFluidProperties,
) -> impl std::future::Future<Output = ()> + 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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> impl std::future::Future<Output = i32> + 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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> impl std::future::Future<Output = Option<FlowingFluidProperties>> + 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<World>,
fluid: &'a Fluid,
pos: &'a BlockPos,
state_id: BlockStateId,
new_props: FlowingFluidProperties,
) -> impl std::future::Future<Output = ()> + 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<World>,
fluid: &'a Fluid,
pos: &'a BlockPos,
) -> impl std::future::Future<Output = bool> + 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<World>,
fluid: &'a Fluid,
pos: &'a BlockPos,
state_id: BlockStateId,
) -> impl std::future::Future<Output = ()> + 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<World>,
fluid: &'a Fluid,
block_pos: &'a BlockPos,
) -> impl std::future::Future<Output = ()> + 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;
}
}
}
}

View File

@@ -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<World>) -> 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<World>,
fluid: &'a Fluid,
pos: &'a BlockPos,
async fn spread_to(
&self,
world: &Arc<World>,
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;
}
}

View File

@@ -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>(

View File

@@ -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<World>, 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<T: FlowingFluid + Sync + ?Sized>(
fluid_impl: &T,
world: &Arc<World>,
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<T: FlowingFluid + Sync + ?Sized>(
fluid_impl: &T,
world: &Arc<World>,
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<usize> {
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
}

View File

@@ -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
}

View File

@@ -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<World>) -> bool {
//TODO add game rule check for water conversion
// TODO: add game rule check for water conversion
true
}
}