Skip to main content

steel_core/fluid/
flowing_fluid.rs

1//! Shared logic for flowing fluids (Water, Lava).
2//!
3//! Provides the `FlowingFluid` trait, which contains the mathematical spread
4//! algorithms derived from vanilla's `FlowingFluid.java`. Individual fluids
5//! like `WaterFluid` and `LavaFluid` implement this trait to inherit behavior.
6
7use std::sync::Arc;
8
9use steel_registry::REGISTRY;
10use steel_registry::blocks::block_state_ext::BlockStateExt;
11use steel_registry::blocks::properties::Direction;
12use steel_registry::vanilla_blocks;
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::{BLOCK_BEHAVIORS, BlockStateBehaviorExt, FLUID_BEHAVIORS};
17use crate::fluid::{
18    FluidBehavior, FluidState, can_hold_any_fluid_state, can_hold_specific_fluid,
19    can_pass_through_wall, fluid_state_to_block, fluid_state_to_block_with_existing,
20    get_new_liquid, get_spread, is_hole,
21};
22use crate::world::World;
23
24/// Trait providing the base algorithm for flowing fluids (Water, Lava).
25/// In vanilla Minecraft, this is the `FlowingFluid` abstract class.
26pub trait FlowingFluid: FluidBehavior {
27    /// The base tick logic
28    fn base_tick(
29        &self,
30        world: &Arc<World>,
31        pos: BlockPos,
32        mut block_state: BlockStateId,
33        mut current_fluid: FluidState,
34    ) {
35        if current_fluid.is_empty() || !self.is_same(current_fluid.fluid_id) {
36            return;
37        }
38
39        if !current_fluid.is_source() {
40            let new_fluid = get_new_liquid(
41                world,
42                pos,
43                block_state,
44                self.fluid_type(),
45                self.drop_off(world),
46            );
47
48            if new_fluid.is_empty() {
49                current_fluid = new_fluid;
50                // Vanilla: unconditionally sets Blocks.AIR when fluid empties
51                block_state = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
52                world.set_block(pos, block_state, UpdateFlags::UPDATE_ALL);
53            } else if new_fluid != current_fluid {
54                let old_fluid = current_fluid;
55                current_fluid = new_fluid;
56                block_state = fluid_state_to_block_with_existing(new_fluid, block_state);
57                world.set_block(pos, block_state, UpdateFlags::UPDATE_ALL);
58
59                world.schedule_fluid_tick_default(
60                    pos,
61                    new_fluid.fluid_id,
62                    self.get_spread_delay(world, pos, old_fluid, new_fluid),
63                );
64            }
65        }
66
67        self.spread(world, pos, block_state, current_fluid);
68    }
69
70    /// The base spread logic.
71    ///
72    /// Vanilla equivalent: `FlowingFluid.spread()`.
73    fn base_spread(
74        &self,
75        world: &Arc<World>,
76        pos: BlockPos,
77        block_state: BlockStateId,
78        fluid_state: FluidState,
79    ) {
80        if fluid_state.is_empty() {
81            return;
82        }
83
84        let below = pos.below();
85        let below_state = world.get_block_state(below);
86        let below_fluid = below_state.get_fluid_state();
87
88        // Vanilla: canMaybePassThrough (source check + canHoldAnyFluid + wall check)
89        //          + canBeReplacedWith + canHoldSpecificFluid
90        let can_spread_down = world.is_in_valid_bounds(below)
91            && !(self.is_same(below_fluid.fluid_id) && below_fluid.is_source())
92            && can_hold_any_fluid_state(below_state)
93            && can_pass_through_wall(world, pos, block_state, below, below_state, Direction::Down);
94
95        if can_spread_down {
96            let new_below_fluid = get_new_liquid(
97                world,
98                below,
99                below_state,
100                self.fluid_type(),
101                self.drop_off(world),
102            );
103
104            if !new_below_fluid.is_empty() {
105                let existing_behavior = FLUID_BEHAVIORS.get_behavior(below_fluid.fluid_id);
106                let can_replace = existing_behavior.can_be_replaced_with(
107                    below_fluid,
108                    world,
109                    below,
110                    new_below_fluid.fluid_id,
111                    Direction::Down,
112                );
113
114                if can_replace && can_hold_specific_fluid(below_state, new_below_fluid.fluid_id) {
115                    self.spread_to(world, below, new_below_fluid, Direction::Down);
116
117                    if self.source_neighbor_count(world, pos) >= 3 {
118                        self.spread_to_sides(world, pos, block_state, fluid_state);
119                    }
120                    return;
121                }
122            }
123        }
124
125        if fluid_state.is_source()
126            || !is_hole(
127                world,
128                pos,
129                block_state,
130                below,
131                below_state,
132                self.fluid_type(),
133            )
134        {
135            self.spread_to_sides(world, pos, block_state, fluid_state);
136        }
137    }
138
139    /// The base logic for placing a fluid into a specific adjacent block.
140    ///
141    /// Vanilla equivalent: `FlowingFluid.spreadTo()`.
142    /// Note: vanilla's spreadTo does NOT schedule ticks — that's handled by
143    /// LiquidBlockContainer.placeLiquid or the new block's onPlace callback.
144    fn base_spread_to(&self, world: &Arc<World>, pos: BlockPos, fluid_state: FluidState) {
145        let target_state = world.get_block_state(pos);
146
147        if target_state.is_liquid_container() {
148            let behavior = BLOCK_BEHAVIORS.get_behavior(target_state.get_block());
149            behavior.place_liquid(world, pos, target_state, fluid_state);
150            return;
151        }
152
153        // Non-LiquidBlockContainer path: destroy the block and place the raw fluid.
154        let target_block = target_state.get_block();
155        if !target_block.config.is_air {
156            self.before_destroying_block(world, pos, target_state);
157        }
158
159        let block_state = fluid_state_to_block(fluid_state);
160        // Vanilla uses flag 3 (UPDATE_ALL). Tick scheduling is handled by
161        // LiquidBlock.on_place which fires from set_block.
162        world.set_block(pos, block_state, UpdateFlags::UPDATE_ALL);
163    }
164
165    /// Performs the actual placement of fluid and schedules the tick.
166    fn spread_to(
167        &self,
168        world: &Arc<World>,
169        pos: BlockPos,
170        fluid_state: FluidState,
171        _direction: Direction,
172    ) {
173        self.base_spread_to(world, pos, fluid_state);
174    }
175
176    /// Returns the number of fluid sources in the 4-directional neighborhood of the given position.
177    fn source_neighbor_count(&self, world: &Arc<World>, pos: BlockPos) -> u8 {
178        let mut count = 0u8;
179        for dir in Direction::HORIZONTAL {
180            let neighbor = dir.relative(pos);
181            let f = world.get_block_state(neighbor).get_fluid_state();
182            if self.is_same(f.fluid_id) && f.is_source() {
183                count += 1;
184            }
185        }
186        count
187    }
188
189    /// Spreads the fluid to horizontal neighbors.
190    ///
191    /// Vanilla equivalent: `FlowingFluid.spreadToSides()`.
192    /// Computes outgoing amount, overrides to 7 for falling fluids, and skips
193    /// if the outgoing amount is zero.
194    fn spread_to_sides(
195        &self,
196        world: &Arc<World>,
197        pos: BlockPos,
198        block_state: BlockStateId,
199        fluid_state: FluidState,
200    ) {
201        // Vanilla: neighbor = amount - dropOff; if (falling) neighbor = 7; if (neighbor <= 0) skip
202        let mut neighbor = fluid_state.amount.saturating_sub(self.drop_off(world));
203        if fluid_state.falling {
204            neighbor = 7;
205        }
206        if neighbor == 0 {
207            return;
208        }
209
210        let spreads = get_spread(
211            world,
212            pos,
213            block_state,
214            self.fluid_type(),
215            self.drop_off(world),
216            self.slope_find_distance(world),
217        );
218
219        for (direction, new_fluid) in spreads {
220            let target: BlockPos = direction.relative(pos);
221            self.spread_to(world, target, new_fluid, direction);
222        }
223    }
224}