Skip to main content

steel_core/behavior/blocks/fluid/
liquid_block.rs

1//! Liquid block behavior (water, lava).
2//!
3//! Based on vanilla's LiquidBlock.java.
4use std::sync::Arc;
5
6use steel_macros::block_behavior;
7use steel_registry::REGISTRY;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::blocks::properties::{BlockStateProperties, Direction};
11use steel_registry::fluid::FluidRef;
12use steel_registry::item_stack::ItemStack;
13use steel_registry::vanilla_block_tags::BlockTag;
14use steel_registry::vanilla_blocks;
15use steel_registry::vanilla_fluid_tags::FluidTag;
16use steel_utils::BlockPos;
17use steel_utils::BlockStateId;
18use steel_utils::types::UpdateFlags;
19
20use steel_registry::level_events;
21use steel_registry::sound_events;
22use steel_registry::vanilla_items;
23
24use crate::behavior::FLUID_BEHAVIORS;
25use crate::behavior::block::{BlockBehavior, PickupResult};
26use crate::behavior::context::BlockPlaceContext;
27use crate::entity::ai::path::PathComputationType;
28use crate::fluid::{FluidStateExt, is_lava_fluid, is_water_fluid};
29use crate::player::Player;
30use crate::world::{ConditionalBlockSetResult, ScheduledTickAccess, World};
31
32use super::BubbleColumnBlock;
33
34/// Behavior for liquid blocks (water and lava).
35///
36/// Liquid blocks have a LEVEL property (0-15) that determines the fluid state:
37/// - LEVEL 0 = source block (full fluid)
38/// - LEVEL 1-7 = flowing fluid with decreasing height
39/// - LEVEL 8-15 = falling fluid
40#[block_behavior]
41pub struct LiquidBlock {
42    block: BlockRef,
43    #[json_arg(vanilla_fluids, ref)]
44    fluid: FluidRef,
45}
46
47impl LiquidBlock {
48    /// Creates a new liquid block behavior.
49    #[must_use]
50    pub const fn new(block: BlockRef, fluid: FluidRef) -> Self {
51        Self { block, fluid }
52    }
53
54    /// Checks if this liquid should spread and handles lava-water interactions.
55    /// Based on vanilla's `LiquidBlock.shouldSpreadLiquid()`.
56    ///
57    /// Returns `true` if the liquid should spread (schedule tick),
58    /// Returns `false` if the liquid was converted to a block (obsidian/cobblestone/basalt).
59    fn should_spread_liquid(&self, world: &Arc<World>, pos: BlockPos) -> bool {
60        // Only lava has special interactions with water and blue ice
61        if !is_lava_fluid(self.fluid) {
62            return true;
63        }
64        // Check if there's soul soil below (for basalt generation)
65        let below_pos = pos.offset(0, -1, 0);
66        let below_state = world.get_block_state(below_pos);
67        let has_soul_soil_below = below_state.get_block() == &vanilla_blocks::SOUL_SOIL;
68
69        // Get fluid state to check if this is a source
70        let fluid_state = world.get_block_state(pos).get_fluid_state();
71
72        for direction in Direction::FLOW_NEIGHBOR_CHECK {
73            let neighbor_pos = direction.relative(pos);
74            let neighbor_fluid = world.get_block_state(neighbor_pos).get_fluid_state();
75
76            // Check for water (including flowing_water and waterlogged blocks)
77            // Using fluid tag check to support modded fluids registered in the water tag
78            if neighbor_fluid.is_water() {
79                // Lava + Water = Obsidian (if source) or Cobblestone (if flowing)
80                let new_block = if fluid_state.is_source() {
81                    &vanilla_blocks::OBSIDIAN
82                } else {
83                    &vanilla_blocks::COBBLESTONE
84                };
85
86                let new_state = REGISTRY.blocks.get_default_state_id(new_block);
87                world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
88                world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
89                return false; // Don't schedule fluid tick - block was converted
90            }
91
92            // Check for basalt generation: soul soil below + blue ice adjacent
93            if has_soul_soil_below {
94                let neighbor_state = world.get_block_state(neighbor_pos);
95                if neighbor_state.get_block() == &vanilla_blocks::BLUE_ICE {
96                    let new_state = REGISTRY
97                        .blocks
98                        .get_default_state_id(&vanilla_blocks::BASALT);
99                    world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
100                    world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
101                    return false; // Don't schedule fluid tick - block was converted
102                }
103            }
104        }
105
106        true // No interaction occurred, proceed with normal fluid tick
107    }
108
109    fn should_bubble_column_occupy(state: BlockStateId) -> bool {
110        let fluid_state = state.get_fluid_state();
111        fluid_state
112            .fluid_id
113            .has_tag(&FluidTag::BUBBLE_COLUMN_CAN_OCCUPY)
114            && fluid_state.is_source()
115            && fluid_state.amount >= 8
116    }
117
118    fn try_schedule_bubble_block_column(
119        &self,
120        ticks: &dyn ScheduledTickAccess,
121        pos: BlockPos,
122        state_below: BlockStateId,
123    ) {
124        let block_below = state_below.get_block();
125        if block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_DRAG_DOWN)
126            || block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_PUSH_UP)
127        {
128            let _ = ticks.schedule_block_tick_default(pos, self.block, 20);
129        }
130    }
131}
132
133impl BlockBehavior for LiquidBlock {
134    fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
135        Some(self.block.default_state())
136    }
137
138    fn is_pathfindable(
139        &self,
140        _state: BlockStateId,
141        _computation_type: PathComputationType,
142    ) -> bool {
143        !is_lava_fluid(self.fluid)
144    }
145
146    /// Called when the block is placed.
147    fn on_place(
148        &self,
149        state: BlockStateId,
150        world: &Arc<World>,
151        pos: BlockPos,
152        _old_state: BlockStateId,
153        _moved_by_piston: bool,
154    ) {
155        if self.should_spread_liquid(world, pos) {
156            let fluid = state.get_fluid_state().fluid_id;
157            let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
158            world.schedule_fluid_tick_default(pos, fluid, delay);
159        }
160
161        if Self::should_bubble_column_occupy(state) {
162            self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
163        }
164    }
165
166    fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
167        if Self::should_bubble_column_occupy(state) {
168            BubbleColumnBlock::update_column(
169                &vanilla_blocks::BUBBLE_COLUMN,
170                world,
171                pos,
172                world.get_block_state(pos.below()),
173            );
174        }
175    }
176
177    /// Called when a neighboring block changes.
178    fn handle_neighbor_changed(
179        &self,
180        state: BlockStateId,
181        world: &Arc<World>,
182        pos: BlockPos,
183        _source_block: BlockRef,
184        _moved_by_piston: bool,
185    ) {
186        if self.should_spread_liquid(world, pos) {
187            let fluid = world.get_block_state(pos).get_fluid_state().fluid_id;
188            let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
189            world.schedule_fluid_tick_default(pos, fluid, delay);
190        }
191
192        if Self::should_bubble_column_occupy(state) {
193            self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
194        }
195    }
196
197    /// Called when a neighbor's shape changes.
198    ///
199    /// Vanilla parity: `LiquidBlock.updateShape` schedules a tick only when
200    /// either the current block or the neighbor contains a source fluid.
201    fn update_shape(
202        &self,
203        state: BlockStateId,
204        world: &dyn ScheduledTickAccess,
205        pos: BlockPos,
206        direction: Direction,
207        _neighbor_pos: BlockPos,
208        neighbor_state: BlockStateId,
209    ) -> BlockStateId {
210        let fluid_state = state.get_fluid_state();
211        let neighbor_fluid = neighbor_state.get_fluid_state();
212
213        if fluid_state.is_source() || neighbor_fluid.is_source() {
214            let delay = world.fluid_tick_delay(fluid_state.fluid_id);
215            world.schedule_fluid_tick_default(pos, fluid_state.fluid_id, delay);
216        }
217
218        if direction == Direction::Down && Self::should_bubble_column_occupy(state) {
219            self.try_schedule_bubble_block_column(world, pos, neighbor_state);
220        }
221
222        state
223    }
224
225    /// Vanilla parity: `LiquidBlock.randomTick` delegates to the fluid.
226    fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
227        FLUID_BEHAVIORS
228            .get_behavior(state.get_fluid_state().fluid_id)
229            .random_tick(world, pos);
230    }
231
232    fn pickup_block(
233        &self,
234        world: &Arc<World>,
235        pos: BlockPos,
236        state: BlockStateId,
237        _player: Option<&Player>,
238    ) -> Option<PickupResult> {
239        if state.try_get_value(&BlockStateProperties::LEVEL) != Some(0) {
240            return None;
241        }
242
243        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
244        if world.set_block_if_unchanged(pos, state, air, UpdateFlags::UPDATE_ALL_IMMEDIATE)
245            != ConditionalBlockSetResult::Changed
246        {
247            return None;
248        }
249
250        let bucket = if is_water_fluid(self.fluid) {
251            &vanilla_items::WATER_BUCKET
252        } else {
253            &vanilla_items::LAVA_BUCKET
254        };
255
256        let sound = if is_water_fluid(self.fluid) {
257            &sound_events::ITEM_BUCKET_FILL
258        } else {
259            &sound_events::ITEM_BUCKET_FILL_LAVA
260        };
261
262        Some(PickupResult {
263            filled_bucket: ItemStack::new(bucket),
264            sound: Some(sound),
265        })
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use crate::behavior::init_behaviors;
272    use steel_registry::{init_vanilla_registry, vanilla_fluids};
273
274    use crate::test_support::TestLevel;
275
276    use super::*;
277
278    #[test]
279    fn update_shape_schedules_actual_flowing_fluid_variant() {
280        init_vanilla_registry();
281        init_behaviors();
282
283        let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
284        let state = vanilla_blocks::WATER
285            .default_state()
286            .set_value(&BlockStateProperties::LEVEL, 1);
287        let neighbor_state = vanilla_blocks::WATER.default_state();
288        let level = TestLevel::default();
289
290        let updated = block.update_shape(
291            state,
292            &level,
293            BlockPos::ZERO,
294            Direction::North,
295            Direction::North.relative(BlockPos::ZERO),
296            neighbor_state,
297        );
298
299        assert_eq!(updated, state);
300        assert_eq!(
301            level
302                .scheduled_fluid_ticks
303                .borrow()
304                .iter()
305                .map(|tick| (tick.fluid, tick.delay))
306                .collect::<Vec<_>>(),
307            vec![(&vanilla_fluids::FLOWING_WATER, 5)]
308        );
309    }
310
311    #[test]
312    fn source_water_above_soul_sand_schedules_bubble_column_tick() {
313        init_vanilla_registry();
314        init_behaviors();
315
316        let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
317        let state = vanilla_blocks::WATER.default_state();
318        let level = TestLevel::default();
319
320        let updated = block.update_shape(
321            state,
322            &level,
323            BlockPos::ZERO,
324            Direction::Down,
325            BlockPos::ZERO.below(),
326            vanilla_blocks::SOUL_SAND.default_state(),
327        );
328
329        assert_eq!(updated, state);
330        assert!(
331            level
332                .scheduled_block_ticks
333                .borrow()
334                .iter()
335                .any(|tick| tick.block == &vanilla_blocks::WATER && tick.delay == 20)
336        );
337    }
338
339    #[test]
340    fn flowing_water_does_not_schedule_bubble_column_tick() {
341        init_vanilla_registry();
342        init_behaviors();
343
344        let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
345        let state = vanilla_blocks::WATER
346            .default_state()
347            .set_value(&BlockStateProperties::LEVEL, 1);
348        let level = TestLevel::default();
349
350        let updated = block.update_shape(
351            state,
352            &level,
353            BlockPos::ZERO,
354            Direction::Down,
355            BlockPos::ZERO.below(),
356            vanilla_blocks::SOUL_SAND.default_state(),
357        );
358
359        assert_eq!(updated, state);
360        assert!(level.scheduled_block_ticks.borrow().is_empty());
361    }
362}