steel_core/behavior/blocks/fluid/
liquid_block.rs1use 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, IntProperty};
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#[block_behavior]
41pub struct LiquidBlock {
42 block: BlockRef,
43 #[json_arg(vanilla_fluids, ref)]
44 fluid: FluidRef,
45}
46
47const LEVEL: &IntProperty = &BlockStateProperties::LEVEL;
48
49impl LiquidBlock {
50 #[must_use]
52 pub const fn new(block: BlockRef, fluid: FluidRef) -> Self {
53 Self { block, fluid }
54 }
55
56 fn should_spread_liquid(&self, world: &Arc<World>, pos: BlockPos) -> bool {
62 if !is_lava_fluid(self.fluid) {
64 return true;
65 }
66 let below_pos = pos.offset(0, -1, 0);
68 let below_state = world.get_block_state(below_pos);
69 let has_soul_soil_below = below_state.get_block() == &vanilla_blocks::SOUL_SOIL;
70
71 let fluid_state = world.get_block_state(pos).get_fluid_state();
73
74 for direction in Direction::FLOW_NEIGHBOR_CHECK {
75 let neighbor_pos = direction.relative(pos);
76 let neighbor_fluid = world.get_block_state(neighbor_pos).get_fluid_state();
77
78 if neighbor_fluid.is_water() {
81 let new_block = if fluid_state.is_source() {
83 &vanilla_blocks::OBSIDIAN
84 } else {
85 &vanilla_blocks::COBBLESTONE
86 };
87
88 let new_state = REGISTRY.blocks.get_default_state_id(new_block);
89 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
90 world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
91 return false; }
93
94 if has_soul_soil_below {
96 let neighbor_state = world.get_block_state(neighbor_pos);
97 if neighbor_state.get_block() == &vanilla_blocks::BLUE_ICE {
98 let new_state = REGISTRY
99 .blocks
100 .get_default_state_id(&vanilla_blocks::BASALT);
101 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
102 world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
103 return false; }
105 }
106 }
107
108 true }
110
111 fn should_bubble_column_occupy(state: BlockStateId) -> bool {
112 let fluid_state = state.get_fluid_state();
113 fluid_state
114 .fluid_id
115 .has_tag(&FluidTag::BUBBLE_COLUMN_CAN_OCCUPY)
116 && fluid_state.is_source()
117 && fluid_state.is_full()
118 }
119
120 fn try_schedule_bubble_block_column(
121 &self,
122 ticks: &dyn ScheduledTickAccess,
123 pos: BlockPos,
124 state_below: BlockStateId,
125 ) {
126 let block_below = state_below.get_block();
127 if block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_DRAG_DOWN)
128 || block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_PUSH_UP)
129 {
130 let _ = ticks.schedule_block_tick_default(pos, self.block, 20);
131 }
132 }
133}
134
135impl BlockBehavior for LiquidBlock {
136 fn is_liquid_block(&self) -> bool {
137 true
138 }
139
140 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
141 Some(self.block.default_state())
142 }
143
144 fn is_pathfindable(
145 &self,
146 _state: BlockStateId,
147 _computation_type: PathComputationType,
148 ) -> bool {
149 !is_lava_fluid(self.fluid)
150 }
151
152 fn on_place(
154 &self,
155 state: BlockStateId,
156 world: &Arc<World>,
157 pos: BlockPos,
158 _old_state: BlockStateId,
159 _moved_by_piston: bool,
160 ) {
161 if self.should_spread_liquid(world, pos) {
162 let fluid = state.get_fluid_state().fluid_id;
163 let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
164 world.schedule_fluid_tick_default(pos, fluid, delay);
165 }
166
167 if Self::should_bubble_column_occupy(state) {
168 self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
169 }
170 }
171
172 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
173 if Self::should_bubble_column_occupy(state) {
174 BubbleColumnBlock::update_column(
175 &vanilla_blocks::BUBBLE_COLUMN,
176 world,
177 pos,
178 world.get_block_state(pos.below()),
179 );
180 }
181 }
182
183 fn handle_neighbor_changed(
185 &self,
186 state: BlockStateId,
187 world: &Arc<World>,
188 pos: BlockPos,
189 _source_block: BlockRef,
190 _moved_by_piston: bool,
191 ) {
192 if self.should_spread_liquid(world, pos) {
193 let fluid = world.get_block_state(pos).get_fluid_state().fluid_id;
194 let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
195 world.schedule_fluid_tick_default(pos, fluid, delay);
196 }
197
198 if Self::should_bubble_column_occupy(state) {
199 self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
200 }
201 }
202
203 fn update_shape(
208 &self,
209 state: BlockStateId,
210 world: &dyn ScheduledTickAccess,
211 pos: BlockPos,
212 direction: Direction,
213 _neighbor_pos: BlockPos,
214 neighbor_state: BlockStateId,
215 ) -> BlockStateId {
216 let fluid_state = state.get_fluid_state();
217 let neighbor_fluid = neighbor_state.get_fluid_state();
218
219 if fluid_state.is_source() || neighbor_fluid.is_source() {
220 let delay = world.fluid_tick_delay(fluid_state.fluid_id);
221 world.schedule_fluid_tick_default(pos, fluid_state.fluid_id, delay);
222 }
223
224 if direction == Direction::Down && Self::should_bubble_column_occupy(state) {
225 self.try_schedule_bubble_block_column(world, pos, neighbor_state);
226 }
227
228 state
229 }
230
231 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
233 FLUID_BEHAVIORS
234 .get_behavior(state.get_fluid_state().fluid_id)
235 .random_tick(world, pos);
236 }
237
238 fn pickup_block(
239 &self,
240 world: &Arc<World>,
241 pos: BlockPos,
242 state: BlockStateId,
243 _player: Option<&Player>,
244 ) -> Option<PickupResult> {
245 if state.try_get_value(LEVEL) != Some(0) {
246 return None;
247 }
248
249 let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
250 if world.set_block_if_unchanged(pos, state, air, UpdateFlags::UPDATE_ALL_IMMEDIATE)
251 != ConditionalBlockSetResult::Changed
252 {
253 return None;
254 }
255
256 let bucket = if is_water_fluid(self.fluid) {
257 &vanilla_items::WATER_BUCKET
258 } else {
259 &vanilla_items::LAVA_BUCKET
260 };
261
262 let sound = if is_water_fluid(self.fluid) {
263 &sound_events::ITEM_BUCKET_FILL
264 } else {
265 &sound_events::ITEM_BUCKET_FILL_LAVA
266 };
267
268 Some(PickupResult {
269 filled_bucket: ItemStack::new(bucket),
270 sound: Some(sound),
271 })
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use crate::behavior::init_behaviors;
278 use steel_registry::{init_vanilla_registry, vanilla_fluids};
279
280 use crate::test_support::TestLevel;
281
282 use super::*;
283
284 #[test]
285 fn update_shape_schedules_actual_flowing_fluid_variant() {
286 init_vanilla_registry();
287 init_behaviors();
288
289 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
290 let state = vanilla_blocks::WATER.default_state().set_value(LEVEL, 1);
291 let neighbor_state = vanilla_blocks::WATER.default_state();
292 let level = TestLevel::default();
293
294 let updated = block.update_shape(
295 state,
296 &level,
297 BlockPos::ZERO,
298 Direction::North,
299 Direction::North.relative(BlockPos::ZERO),
300 neighbor_state,
301 );
302
303 assert_eq!(updated, state);
304 assert_eq!(
305 level
306 .scheduled_fluid_ticks
307 .borrow()
308 .iter()
309 .map(|tick| (tick.fluid, tick.delay))
310 .collect::<Vec<_>>(),
311 vec![(&vanilla_fluids::FLOWING_WATER, 5)]
312 );
313 }
314
315 #[test]
316 fn source_water_above_soul_sand_schedules_bubble_column_tick() {
317 init_vanilla_registry();
318 init_behaviors();
319
320 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
321 let state = vanilla_blocks::WATER.default_state();
322 let level = TestLevel::default();
323
324 let updated = block.update_shape(
325 state,
326 &level,
327 BlockPos::ZERO,
328 Direction::Down,
329 BlockPos::ZERO.below(),
330 vanilla_blocks::SOUL_SAND.default_state(),
331 );
332
333 assert_eq!(updated, state);
334 assert!(
335 level
336 .scheduled_block_ticks
337 .borrow()
338 .iter()
339 .any(|tick| tick.block == &vanilla_blocks::WATER && tick.delay == 20)
340 );
341 }
342
343 #[test]
344 fn flowing_water_does_not_schedule_bubble_column_tick() {
345 init_vanilla_registry();
346 init_behaviors();
347
348 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
349 let state = vanilla_blocks::WATER.default_state().set_value(LEVEL, 1);
350 let level = TestLevel::default();
351
352 let updated = block.update_shape(
353 state,
354 &level,
355 BlockPos::ZERO,
356 Direction::Down,
357 BlockPos::ZERO.below(),
358 vanilla_blocks::SOUL_SAND.default_state(),
359 );
360
361 assert_eq!(updated, state);
362 assert!(level.scheduled_block_ticks.borrow().is_empty());
363 }
364
365 #[test]
366 fn liquid_block_behavior_and_ext_identify_liquid_blocks() {
367 use crate::behavior::BlockStateBehaviorExt as _;
368
369 init_vanilla_registry();
370 init_behaviors();
371
372 let liquid = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
373 assert!(liquid.is_liquid_block());
374
375 assert!(vanilla_blocks::WATER.default_state().is_liquid_block());
376 assert!(vanilla_blocks::LAVA.default_state().is_liquid_block());
377 assert!(!vanilla_blocks::STONE.default_state().is_liquid_block());
378 assert!(!vanilla_blocks::AIR.default_state().is_liquid_block());
379 }
380}