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};
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
47impl LiquidBlock {
48 #[must_use]
50 pub const fn new(block: BlockRef, fluid: FluidRef) -> Self {
51 Self { block, fluid }
52 }
53
54 fn should_spread_liquid(&self, world: &Arc<World>, pos: BlockPos) -> bool {
60 if !is_lava_fluid(self.fluid) {
62 return true;
63 }
64 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 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 if neighbor_fluid.is_water() {
79 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; }
91
92 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; }
103 }
104 }
105
106 true }
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 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 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 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 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}