mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
chore: optimized, working fluids (#1392)
* fix: optimized, working fluids * chore: clippy and formatting
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
547
pumpkin/src/block/fluid/flowing_trait.rs
Normal file
547
pumpkin/src/block/fluid/flowing_trait.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
225
pumpkin/src/block/fluid/pathfinder.rs
Normal file
225
pumpkin/src/block/fluid/pathfinder.rs
Normal 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
|
||||
}
|
||||
51
pumpkin/src/block/fluid/physics.rs
Normal file
51
pumpkin/src/block/fluid/physics.rs
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user