1use std::sync::Arc;
6
7use crate::behavior::FLUID_BEHAVIORS;
8use crate::fluid::can_pass_through_wall;
9use crate::fluid::collision::{
10 can_hold_fluid, can_hold_specific_fluid, can_pass_horizontally_internal,
11};
12use crate::fluid::spread_context::SpreadContext;
13use crate::world::World;
14use steel_registry::blocks::block_state_ext::BlockStateExt;
15use steel_registry::blocks::properties::{BlockStateProperties, Direction};
16use steel_registry::fluid::{FluidRef, FluidState};
17use steel_utils::{BlockPos, BlockStateId};
18
19#[must_use]
21pub fn get_new_liquid(
22 world: &Arc<World>,
23 pos: BlockPos,
24 state: BlockStateId,
25 fluid_id: FluidRef,
26 drop_off: u8,
27) -> FluidState {
28 let behavior = FLUID_BEHAVIORS.get_behavior(fluid_id);
29 let mut max_incoming_amount = 0u8;
30 let mut source_count = 0u8;
31
32 for direction in [
33 Direction::North,
34 Direction::South,
35 Direction::East,
36 Direction::West,
37 ] {
38 let neighbor_pos = direction.relative(pos);
39 let neighbor_state = world.get_block_state(neighbor_pos);
40 let neighbor_fluid = neighbor_state.get_fluid_state();
41
42 if !behavior.is_same(neighbor_fluid.fluid_id) {
43 continue;
44 }
45
46 if !can_pass_through_wall(world, pos, state, neighbor_pos, neighbor_state, direction) {
47 continue;
48 }
49
50 if neighbor_fluid.is_source() {
51 source_count += 1;
52 max_incoming_amount = max_incoming_amount.max(8u8.saturating_sub(drop_off));
53 } else {
54 max_incoming_amount =
55 max_incoming_amount.max(neighbor_fluid.amount.saturating_sub(drop_off));
56 }
57 }
58
59 if source_count >= 2 && behavior.can_convert_to_source(world) {
62 let below_pos = pos.below();
63 let below_state = world.get_block_state(below_pos);
64 let below_fluid = below_state.get_fluid_state();
65 if below_state.is_solid()
66 || (behavior.is_same(below_fluid.fluid_id) && below_fluid.is_source())
67 {
68 return FluidState::source(fluid_id.source_variant());
69 }
70 }
71
72 let above_pos = pos.above();
74 let above_state = world.get_block_state(above_pos);
75 let above_fluid = above_state.get_fluid_state();
76 if behavior.is_same(above_fluid.fluid_id)
77 && can_pass_through_wall(world, pos, state, above_pos, above_state, Direction::Up)
78 {
79 return FluidState::flowing(fluid_id.flowing_variant(), 8, true);
80 }
81
82 if max_incoming_amount > 0 {
83 FluidState::flowing(fluid_id.flowing_variant(), max_incoming_amount, false)
84 } else {
85 FluidState::EMPTY
86 }
87}
88
89#[must_use]
94pub fn is_hole(
95 world: &Arc<World>,
96 top_pos: BlockPos,
97 top_state: BlockStateId,
98 bottom_pos: BlockPos,
99 bottom_state: BlockStateId,
100 fluid_id: FluidRef,
101) -> bool {
102 if !world.is_in_valid_bounds(bottom_pos) {
103 return false;
104 }
105
106 if !can_pass_through_wall(
107 world,
108 top_pos,
109 top_state,
110 bottom_pos,
111 bottom_state,
112 Direction::Down,
113 ) {
114 return false;
115 }
116
117 can_flow_down_into(bottom_state, bottom_state.get_fluid_state(), fluid_id)
118}
119
120fn can_flow_down_into(
121 below_state: steel_utils::BlockStateId,
122 below_fluid: FluidState,
123 fluid_id: FluidRef,
124) -> bool {
125 if FLUID_BEHAVIORS
129 .get_behavior(fluid_id)
130 .is_same(below_fluid.fluid_id)
131 {
132 return true;
133 }
134
135 can_hold_fluid(below_state, fluid_id.flowing_variant())
136}
137
138#[must_use]
143fn get_slope_distance(
144 ctx: &mut SpreadContext,
145 pos: BlockPos,
146 state: BlockStateId,
147 depth: u8,
148 from_direction: Option<Direction>,
149 fluid_id: FluidRef,
150 max_depth: u8,
151) -> u16 {
152 let mut min_distance: u16 = 1000;
153
154 for direction in [
156 Direction::North,
157 Direction::South,
158 Direction::East,
159 Direction::West,
160 ] {
161 if let Some(from) = from_direction
163 && direction == from.opposite()
164 {
165 continue;
166 }
167
168 let neighbor = direction.relative(pos);
169 let neighbor_state = ctx.get_block_state(neighbor);
170
171 if !can_pass_horizontally_internal(neighbor_state, fluid_id) {
173 continue;
174 }
175
176 if !can_pass_through_wall(ctx.world(), pos, state, neighbor, neighbor_state, direction) {
177 continue;
178 }
179
180 if !can_hold_specific_fluid(neighbor_state, fluid_id.flowing_variant()) {
183 continue;
184 }
185
186 if ctx.is_hole(neighbor, fluid_id) {
187 return u16::from(depth); }
189
190 if depth < max_depth {
192 let distance = get_slope_distance(
193 ctx,
194 neighbor,
195 neighbor_state,
196 depth + 1,
197 Some(direction),
198 fluid_id,
199 max_depth,
200 );
201 if distance < min_distance {
202 min_distance = distance;
203 }
204 }
205 }
206
207 min_distance
208}
209
210#[must_use]
219pub fn get_spread(
220 world: &Arc<World>,
221 pos: BlockPos,
222 state: BlockStateId,
223 fluid_id: FluidRef,
224 drop_off: u8,
225 slope_find_distance: u8,
226) -> Vec<(Direction, FluidState)> {
227 let mut candidates: Vec<(Direction, FluidState, FluidState, u16)> = Vec::new();
228 let mut ctx: Option<SpreadContext<'_>> = None;
232
233 for direction in [
234 Direction::North,
235 Direction::South,
236 Direction::East,
237 Direction::West,
238 ] {
239 let neighbor = direction.relative(pos);
240 let neighbor_state = match &mut ctx {
241 Some(ctx) => ctx.get_block_state(neighbor),
242 None => world.get_block_state(neighbor),
243 };
244 let neighbor_fluid = neighbor_state.get_fluid_state();
245
246 if !can_pass_horizontally_internal(neighbor_state, fluid_id) {
248 continue;
249 }
250 if !can_pass_through_wall(world, pos, state, neighbor, neighbor_state, direction) {
251 continue;
252 }
253
254 let new_fluid = get_new_liquid(world, neighbor, neighbor_state, fluid_id, drop_off);
256
257 if !can_hold_specific_fluid(neighbor_state, new_fluid.fluid_id) {
259 continue;
260 }
261
262 if neighbor_state
266 .try_get_value(&BlockStateProperties::WATERLOGGED)
267 .is_some()
268 && !new_fluid.is_source()
269 {
270 continue;
271 }
272
273 if new_fluid.is_empty() {
275 continue;
276 }
277
278 let ctx = ctx.get_or_insert_with(|| SpreadContext::new(world, pos));
280 ctx.cache_block_state(neighbor, neighbor_state);
281 let distance = if ctx.is_hole(neighbor, fluid_id) {
282 0
283 } else if slope_find_distance > 0 {
284 get_slope_distance(
285 ctx,
286 neighbor,
287 neighbor_state,
288 1,
289 Some(direction),
290 fluid_id,
291 slope_find_distance,
292 )
293 } else {
294 1000
295 };
296
297 candidates.push((direction, new_fluid, neighbor_fluid, distance));
299 }
300
301 if candidates.is_empty() {
302 return Vec::new();
303 }
304
305 let min_distance = candidates
307 .iter()
308 .map(|(_, _, _, distance)| *distance)
309 .min()
310 .unwrap_or(1000);
311
312 candidates
315 .into_iter()
316 .filter(|(direction, new_fluid, existing_fluid, distance)| {
317 if *distance != min_distance {
318 return false;
319 }
320 let neighbor = direction.relative(pos);
321
322 let existing_behavior = FLUID_BEHAVIORS.get_behavior(existing_fluid.fluid_id);
323 existing_behavior.can_be_replaced_with(
324 *existing_fluid,
325 world,
326 neighbor,
327 new_fluid.fluid_id,
328 *direction,
329 )
330 })
331 .map(|(direction, fluid, _, _)| (direction, fluid))
332 .collect()
333}
334
335#[cfg(test)]
336mod tests {
337 use crate::behavior::init_behaviors;
338 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
339 use steel_registry::blocks::properties::BlockStateProperties;
340 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
341 use steel_utils::ChunkPos;
342 use steel_utils::types::UpdateFlags;
343
344 use super::*;
345
346 #[test]
347 fn hole_check_rejects_dry_waterloggable_for_flowing_water_fallback() {
348 init_vanilla_registry();
349 init_behaviors();
350
351 let dry_waterloggable = vanilla_blocks::OAK_LEAVES
352 .default_state()
353 .set_value(&BlockStateProperties::WATERLOGGED, false);
354
355 assert!(!can_flow_down_into(
356 dry_waterloggable,
357 FluidState::EMPTY,
358 &vanilla_fluids::WATER
359 ));
360 }
361
362 #[test]
363 fn hole_check_treats_source_and_flowing_variants_as_same_fluid_below() {
364 init_vanilla_registry();
365 init_behaviors();
366
367 let flowing_water = vanilla_blocks::WATER
368 .default_state()
369 .set_value(&BlockStateProperties::LEVEL, 1);
370
371 assert!(can_flow_down_into(
372 flowing_water,
373 flowing_water.get_fluid_state(),
374 &vanilla_fluids::WATER
375 ));
376 }
377
378 #[test]
379 fn spread_keeps_the_closest_slope_even_when_its_target_rejects_replacement() {
380 init_vanilla_registry();
381 init_behaviors();
382
383 let world = fresh_test_world("fluid_spread_closest_slope");
384 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
385 let origin = BlockPos::new(8, 64, 8);
386 let source_water = vanilla_blocks::WATER.default_state();
387 let thin_lava = vanilla_blocks::LAVA
388 .default_state()
389 .set_value(&BlockStateProperties::LEVEL, 7);
390 let stone = vanilla_blocks::STONE.default_state();
391 let flags = UpdateFlags::UPDATE_NONE | UpdateFlags::UPDATE_SKIP_ON_PLACE;
392
393 for (pos, state) in [
394 (origin, source_water),
395 (origin.north(), thin_lava),
396 (origin.south().below(), stone),
397 (origin.east(), stone),
398 (origin.west(), stone),
399 ] {
400 assert!(world.set_block_with_limit(pos, state, flags, 0));
401 }
402
403 let blocked = get_spread(&world, origin, source_water, &vanilla_fluids::WATER, 1, 4);
404 assert!(blocked.is_empty());
405
406 assert!(world.set_block_with_limit(
407 origin.north(),
408 vanilla_blocks::AIR.default_state(),
409 flags,
410 0,
411 ));
412 let open = get_spread(&world, origin, source_water, &vanilla_fluids::WATER, 1, 4);
413 assert_eq!(
414 open,
415 vec![(
416 Direction::North,
417 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 7, false),
418 )]
419 );
420 }
421}