steel_core/behavior/blocks/fluid/
liquid_block.rs1use std::sync::Arc;
3
4use steel_macros::block_behavior;
5use steel_registry::REGISTRY;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt;
8use steel_registry::blocks::properties::{BlockStateProperties, Direction, IntProperty};
9use steel_registry::fluid::FluidRef;
10use steel_registry::item_stack::ItemStack;
11use steel_registry::vanilla_block_tags::BlockTag;
12use steel_registry::vanilla_blocks;
13use steel_registry::vanilla_fluid_tags::FluidTag;
14use steel_utils::BlockPos;
15use steel_utils::BlockStateId;
16use steel_utils::types::UpdateFlags;
17
18use steel_registry::level_events;
19use steel_registry::vanilla_items;
20
21use crate::behavior::FLUID_BEHAVIORS;
22use crate::behavior::block::{BlockBehavior, PickupResult};
23use crate::behavior::context::BlockPlaceContext;
24use crate::entity::ai::path::PathComputationType;
25use crate::fluid::{FluidStateExt, is_lava_fluid};
26use crate::player::Player;
27use crate::world::{ConditionalBlockSetResult, ScheduledTickAccess, World};
28
29use super::BubbleColumnBlock;
30
31#[block_behavior]
38pub struct LiquidBlock {
39 block: BlockRef,
40 #[json_arg(vanilla_fluids, ref)]
41 fluid: FluidRef,
42}
43
44const LEVEL: &IntProperty = &BlockStateProperties::LEVEL;
45
46impl LiquidBlock {
47 #[must_use]
49 pub const fn new(block: BlockRef, fluid: FluidRef) -> Self {
50 Self { block, fluid }
51 }
52
53 fn should_spread_liquid(&self, world: &Arc<World>, pos: BlockPos) -> bool {
58 if !is_lava_fluid(self.fluid) {
60 return true;
61 }
62 let below_pos = pos.offset(0, -1, 0);
63 let below_state = world.get_block_state(below_pos);
64 let has_soul_soil_below = below_state.get_block() == &vanilla_blocks::SOUL_SOIL;
65
66 let fluid_state = world.get_block_state(pos).get_fluid_state();
67
68 for direction in Direction::FLOW_NEIGHBOR_CHECK {
69 let neighbor_pos = direction.relative(pos);
70 let neighbor_fluid = world.get_block_state(neighbor_pos).get_fluid_state();
71
72 if neighbor_fluid.is_water() {
74 let new_block = if fluid_state.is_source() {
75 &vanilla_blocks::OBSIDIAN
76 } else {
77 &vanilla_blocks::COBBLESTONE
78 };
79
80 Self::convert_and_fizz(world, pos, new_block);
81 return false;
82 }
83
84 if has_soul_soil_below {
85 let neighbor_state = world.get_block_state(neighbor_pos);
86 if neighbor_state.get_block() == &vanilla_blocks::BLUE_ICE {
87 Self::convert_and_fizz(world, pos, &vanilla_blocks::BASALT);
88 return false;
89 }
90 }
91 }
92
93 true
94 }
95
96 fn convert_and_fizz(world: &Arc<World>, pos: BlockPos, block: BlockRef) {
98 world.set_block(pos, block.default_state(), UpdateFlags::UPDATE_ALL);
99 world.level_event(level_events::LAVA_FIZZ, pos, 0, None);
100 }
101
102 fn should_bubble_column_occupy(state: BlockStateId) -> bool {
103 let fluid_state = state.get_fluid_state();
104 fluid_state
105 .fluid_id
106 .has_tag(&FluidTag::BUBBLE_COLUMN_CAN_OCCUPY)
107 && fluid_state.is_source()
108 && fluid_state.is_full()
109 }
110
111 fn try_schedule_bubble_block_column(
112 &self,
113 ticks: &dyn ScheduledTickAccess,
114 pos: BlockPos,
115 state_below: BlockStateId,
116 ) {
117 let block_below = state_below.get_block();
118 if block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_DRAG_DOWN)
119 || block_below.has_tag(&BlockTag::ENABLES_BUBBLE_COLUMN_PUSH_UP)
120 {
121 let _ = ticks.schedule_block_tick_default(pos, self.block, 20);
122 }
123 }
124}
125
126impl BlockBehavior for LiquidBlock {
127 fn is_liquid_block(&self) -> bool {
128 true
129 }
130
131 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
132 Some(self.block.default_state())
133 }
134
135 fn is_pathfindable(
136 &self,
137 _state: BlockStateId,
138 _computation_type: PathComputationType,
139 ) -> bool {
140 !is_lava_fluid(self.fluid)
141 }
142
143 fn on_place(
145 &self,
146 state: BlockStateId,
147 world: &Arc<World>,
148 pos: BlockPos,
149 _old_state: BlockStateId,
150 _moved_by_piston: bool,
151 ) {
152 if self.should_spread_liquid(world, pos) {
153 let fluid = state.get_fluid_state().fluid_id;
154 let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
155 world.schedule_fluid_tick_default(pos, fluid, delay);
156 }
157
158 if Self::should_bubble_column_occupy(state) {
159 self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
160 }
161 }
162
163 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
164 if Self::should_bubble_column_occupy(state) {
165 BubbleColumnBlock::update_column(
166 &vanilla_blocks::BUBBLE_COLUMN,
167 world,
168 pos,
169 world.get_block_state(pos.below()),
170 );
171 }
172 }
173
174 fn handle_neighbor_changed(
176 &self,
177 state: BlockStateId,
178 world: &Arc<World>,
179 pos: BlockPos,
180 _source_block: BlockRef,
181 _moved_by_piston: bool,
182 ) {
183 if self.should_spread_liquid(world, pos) {
184 let fluid = world.get_block_state(pos).get_fluid_state().fluid_id;
185 let delay = FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(world);
186 world.schedule_fluid_tick_default(pos, fluid, delay);
187 }
188
189 if Self::should_bubble_column_occupy(state) {
190 self.try_schedule_bubble_block_column(world, pos, world.get_block_state(pos.below()));
191 }
192 }
193
194 fn update_shape(
199 &self,
200 state: BlockStateId,
201 world: &dyn ScheduledTickAccess,
202 pos: BlockPos,
203 direction: Direction,
204 _neighbor_pos: BlockPos,
205 neighbor_state: BlockStateId,
206 ) -> BlockStateId {
207 let fluid_state = state.get_fluid_state();
208 let neighbor_fluid = neighbor_state.get_fluid_state();
209
210 if fluid_state.is_source() || neighbor_fluid.is_source() {
211 let delay = world.fluid_tick_delay(fluid_state.fluid_id);
212 world.schedule_fluid_tick_default(pos, fluid_state.fluid_id, delay);
213 }
214
215 if direction == Direction::Down && Self::should_bubble_column_occupy(state) {
216 self.try_schedule_bubble_block_column(world, pos, neighbor_state);
217 }
218
219 state
220 }
221
222 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
223 FLUID_BEHAVIORS
224 .get_behavior(state.get_fluid_state().fluid_id)
225 .random_tick(world, pos);
226 }
227
228 fn pickup_block(
229 &self,
230 world: &Arc<World>,
231 pos: BlockPos,
232 state: BlockStateId,
233 _player: Option<&Player>,
234 ) -> Option<PickupResult> {
235 if state.try_get_value(LEVEL) != Some(0) {
236 return None;
237 }
238 let behavior = FLUID_BEHAVIORS.get_behavior(self.fluid);
239 let bucket = behavior.bucket();
241 if bucket == &*vanilla_items::AIR {
242 return None;
243 }
244
245 let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
246 if world.set_block_if_unchanged(pos, state, air, UpdateFlags::UPDATE_ALL_IMMEDIATE)
247 != ConditionalBlockSetResult::Changed
248 {
249 return None;
250 }
251
252 Some(PickupResult {
253 filled_bucket: ItemStack::new(bucket),
254 sound: behavior.pickup_sound(),
255 })
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use crate::behavior::init_behaviors;
262 use steel_registry::{init_vanilla_registry, sound_events, vanilla_fluids};
263
264 use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
265 use steel_utils::ChunkPos;
266
267 use super::*;
268
269 #[test]
270 fn update_shape_schedules_actual_flowing_fluid_variant() {
271 init_vanilla_registry();
272 init_behaviors();
273
274 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
275 let state = vanilla_blocks::WATER.default_state().set_value(LEVEL, 1);
276 let neighbor_state = vanilla_blocks::WATER.default_state();
277 let level = TestLevel::default();
278
279 let updated = block.update_shape(
280 state,
281 &level,
282 BlockPos::ZERO,
283 Direction::North,
284 Direction::North.relative(BlockPos::ZERO),
285 neighbor_state,
286 );
287
288 assert_eq!(updated, state);
289 assert_eq!(
290 level
291 .scheduled_fluid_ticks
292 .borrow()
293 .iter()
294 .map(|tick| (tick.fluid, tick.delay))
295 .collect::<Vec<_>>(),
296 vec![(&vanilla_fluids::FLOWING_WATER, 5)]
297 );
298 }
299
300 #[test]
301 fn source_water_above_soul_sand_schedules_bubble_column_tick() {
302 init_vanilla_registry();
303 init_behaviors();
304
305 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
306 let state = vanilla_blocks::WATER.default_state();
307 let level = TestLevel::default();
308
309 let updated = block.update_shape(
310 state,
311 &level,
312 BlockPos::ZERO,
313 Direction::Down,
314 BlockPos::ZERO.below(),
315 vanilla_blocks::SOUL_SAND.default_state(),
316 );
317
318 assert_eq!(updated, state);
319 assert!(
320 level
321 .scheduled_block_ticks
322 .borrow()
323 .iter()
324 .any(|tick| tick.block == &vanilla_blocks::WATER && tick.delay == 20)
325 );
326 }
327
328 #[test]
329 fn flowing_water_does_not_schedule_bubble_column_tick() {
330 init_vanilla_registry();
331 init_behaviors();
332
333 let block = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
334 let state = vanilla_blocks::WATER.default_state().set_value(LEVEL, 1);
335 let level = TestLevel::default();
336
337 let updated = block.update_shape(
338 state,
339 &level,
340 BlockPos::ZERO,
341 Direction::Down,
342 BlockPos::ZERO.below(),
343 vanilla_blocks::SOUL_SAND.default_state(),
344 );
345
346 assert_eq!(updated, state);
347 assert!(level.scheduled_block_ticks.borrow().is_empty());
348 }
349
350 #[test]
351 fn liquid_block_behavior_and_ext_identify_liquid_blocks() {
352 use crate::behavior::BlockStateBehaviorExt as _;
353
354 init_vanilla_registry();
355 init_behaviors();
356
357 let liquid = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
358 assert!(liquid.is_liquid_block());
359
360 assert!(vanilla_blocks::WATER.default_state().is_liquid_block());
361 assert!(vanilla_blocks::LAVA.default_state().is_liquid_block());
362 assert!(!vanilla_blocks::STONE.default_state().is_liquid_block());
363 assert!(!vanilla_blocks::AIR.default_state().is_liquid_block());
364 }
365
366 #[test]
367 fn pickup_returns_the_fluid_bucket_and_sound_from_sources_only() {
368 init_vanilla_registry();
369 init_behaviors();
370 let fixture = fresh_test_world("liquid_pickup");
371 let world = &fixture.world;
372 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
373 let pos = BlockPos::new(1, 64, 1);
374 let cases = [
375 (
376 &vanilla_blocks::WATER,
377 &vanilla_fluids::WATER,
378 &*vanilla_items::WATER_BUCKET,
379 &sound_events::ITEM_BUCKET_FILL,
380 ),
381 (
382 &vanilla_blocks::LAVA,
383 &vanilla_fluids::LAVA,
384 &*vanilla_items::LAVA_BUCKET,
385 &sound_events::ITEM_BUCKET_FILL_LAVA,
386 ),
387 ];
388 for (block, fluid, bucket, sound) in cases {
389 let liquid = LiquidBlock::new(block, fluid);
390 let flowing = block.default_state().set_value(LEVEL, 3_u8);
391 world.set_block(pos, flowing, UpdateFlags::UPDATE_CLIENTS);
392 assert!(liquid.pickup_block(world, pos, flowing, None).is_none());
393
394 let source = block.default_state();
395 world.set_block(pos, source, UpdateFlags::UPDATE_CLIENTS);
396 let Some(picked) = liquid.pickup_block(world, pos, source, None) else {
397 panic!("{} source should be picked up", fluid.key);
398 };
399 assert_eq!(picked.filled_bucket.item().key, bucket.key);
400 assert_eq!(picked.sound.map(|event| &event.key), Some(&sound.key));
401 assert!(world.get_block_state(pos).is_air());
402 }
403 }
404}