1use std::sync::Arc;
7
8use glam::DVec3;
9use steel_registry::REGISTRY;
10use steel_registry::blocks::BlockRef;
11use steel_registry::blocks::block_state_ext::BlockStateExt;
12use steel_registry::blocks::properties::BlockStateProperties;
13use steel_registry::fluid::{FluidRef, FluidState};
14use steel_registry::vanilla_blocks;
15use steel_utils::{BlockPos, BlockStateId, Direction};
16
17use crate::behavior::FLUID_BEHAVIORS;
18use crate::world::World;
19use steel_registry::vanilla_fluids;
20
21const FLOW_BELOW_HEIGHT_OFFSET: f32 = 0.888_888_9;
22const FALLING_FLOW_DOWNWARD: f64 = -6.0;
23
24#[must_use]
28pub fn get_fluid_state(world: &Arc<World>, pos: BlockPos) -> FluidState {
29 let state = world.get_block_state(pos);
30 get_fluid_state_from_block(state)
31}
32
33#[must_use]
35pub fn get_fluid_state_from_block(state: BlockStateId) -> FluidState {
36 state.get_fluid_state()
37}
38
39#[must_use]
45pub fn fluid_state_to_block_with_existing(
46 fluid_state: FluidState,
47 existing_state: BlockStateId,
48) -> BlockStateId {
49 let fluid_id = fluid_state.fluid_id;
50 if fluid_id.is_empty {
51 if existing_state
54 .try_get_value(&BlockStateProperties::WATERLOGGED)
55 .is_some()
56 {
57 return existing_state.set_value(&BlockStateProperties::WATERLOGGED, false);
58 }
59 return REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
60 }
61
62 if fluid_id == &vanilla_fluids::WATER
63 && existing_state
64 .try_get_value(&BlockStateProperties::WATERLOGGED)
65 .is_some()
66 {
67 return existing_state.set_value(&BlockStateProperties::WATERLOGGED, true);
69 }
70
71 fluid_state_to_block(fluid_state)
72}
73
74#[must_use]
76pub fn liquid_block_state(block: BlockRef, fluid_state: FluidState) -> BlockStateId {
77 block
78 .default_state()
79 .set_value(&BlockStateProperties::LEVEL, fluid_state.to_block_level())
80}
81
82#[must_use]
84pub fn fluid_state_to_block(fluid_state: FluidState) -> BlockStateId {
85 FLUID_BEHAVIORS
86 .get_behavior(fluid_state.fluid_id)
87 .create_legacy_block(fluid_state)
88}
89
90#[must_use]
92pub fn water_id() -> FluidRef {
93 &vanilla_fluids::WATER
94}
95
96#[must_use]
98pub fn lava_id() -> FluidRef {
99 &vanilla_fluids::LAVA
100}
101
102#[must_use]
106pub fn get_own_height(fluid_state: FluidState) -> f32 {
107 f32::from(fluid_state.amount) / 9.0
108}
109
110#[must_use]
114pub fn get_height(world: &Arc<World>, pos: BlockPos, fluid_state: FluidState) -> f32 {
115 if fluid_state.is_empty() {
116 return 0.0;
117 }
118
119 let above = pos.offset(0, 1, 0);
120 let above_fluid = get_fluid_state(world, above);
121 let behavior = FLUID_BEHAVIORS.get_behavior(fluid_state.fluid_id);
122 get_height_with(fluid_state, above_fluid, |candidate| {
123 behavior.is_same(candidate.fluid_id)
124 })
125}
126
127fn get_height_with<S>(fluid_state: FluidState, above_fluid: FluidState, same_fluid: S) -> f32
128where
129 S: Fn(FluidState) -> bool,
130{
131 if fluid_state.is_empty() {
132 return 0.0;
133 }
134
135 if same_fluid(above_fluid) {
136 1.0
137 } else {
138 get_own_height(fluid_state)
139 }
140}
141
142#[must_use]
145pub fn get_flow(world: &Arc<World>, pos: BlockPos, fluid_state: FluidState) -> DVec3 {
146 if fluid_state.is_empty() {
147 return DVec3::ZERO;
148 }
149
150 let behavior = FLUID_BEHAVIORS.get_behavior(fluid_state.fluid_id);
151 get_flow_with(
152 pos,
153 fluid_state,
154 |candidate| behavior.is_same(candidate.fluid_id),
155 |fluid_pos| get_fluid_state(world, fluid_pos),
156 |block_pos| world.get_block_state(block_pos),
157 )
158}
159
160fn get_flow_with<S, F, B>(
161 pos: BlockPos,
162 fluid_state: FluidState,
163 same_fluid: S,
164 mut fluid_at: F,
165 mut block_at: B,
166) -> DVec3
167where
168 S: Fn(FluidState) -> bool,
169 F: FnMut(BlockPos) -> FluidState,
170 B: FnMut(BlockPos) -> BlockStateId,
171{
172 if fluid_state.is_empty() {
173 return DVec3::ZERO;
174 }
175
176 let own_height = get_own_height(fluid_state);
177 let mut flow = DVec3::ZERO;
178 for direction in Direction::HORIZONTAL {
179 let neighbor_pos = direction.relative(pos);
180 let neighbor_fluid = fluid_at(neighbor_pos);
181 if !affects_flow_with(neighbor_fluid, &same_fluid) {
182 continue;
183 }
184
185 let mut neighbor_height = get_own_height(neighbor_fluid);
186 let mut distance = 0.0;
187 if neighbor_height == 0.0 {
188 if !block_at(neighbor_pos).blocks_motion() {
189 let below_fluid = fluid_at(neighbor_pos.below());
190 if affects_flow_with(below_fluid, &same_fluid) {
191 neighbor_height = get_own_height(below_fluid);
192 if neighbor_height > 0.0 {
193 distance = own_height - (neighbor_height - FLOW_BELOW_HEIGHT_OFFSET);
194 }
195 }
196 }
197 } else {
198 distance = own_height - neighbor_height;
199 }
200
201 if distance != 0.0 {
202 let (dx, dz) = direction.offset_xz();
203 flow.x += f64::from(dx) * f64::from(distance);
204 flow.z += f64::from(dz) * f64::from(distance);
205 }
206 }
207
208 if fluid_state.falling {
209 for direction in Direction::HORIZONTAL {
210 let neighbor_pos = direction.relative(pos);
211 if is_solid_face_with(
212 neighbor_pos,
213 direction,
214 &same_fluid,
215 &mut fluid_at,
216 &mut block_at,
217 ) || is_solid_face_with(
218 neighbor_pos.above(),
219 direction,
220 &same_fluid,
221 &mut fluid_at,
222 &mut block_at,
223 ) {
224 flow = flow.normalize_or_zero() + DVec3::new(0.0, FALLING_FLOW_DOWNWARD, 0.0);
225 break;
226 }
227 }
228 }
229
230 flow.normalize_or_zero()
231}
232
233fn affects_flow_with<S>(neighbor_fluid: FluidState, same_fluid: &S) -> bool
234where
235 S: Fn(FluidState) -> bool,
236{
237 neighbor_fluid.is_empty() || same_fluid(neighbor_fluid)
238}
239
240fn is_solid_face_with<S, F, B>(
241 pos: BlockPos,
242 direction: Direction,
243 same_fluid: &S,
244 fluid_at: &mut F,
245 block_at: &mut B,
246) -> bool
247where
248 S: Fn(FluidState) -> bool,
249 F: FnMut(BlockPos) -> FluidState,
250 B: FnMut(BlockPos) -> BlockStateId,
251{
252 let state = block_at(pos);
253 let fluid_state = fluid_at(pos);
254 if same_fluid(fluid_state) {
255 return false;
256 }
257 if direction == Direction::Up {
258 return true;
259 }
260 state.get_block() != &vanilla_blocks::ICE && state.is_face_sturdy_at(pos, direction)
261}
262
263#[cfg(test)]
264mod tests {
265 use crate::behavior::init_behaviors;
266 use steel_registry::fluid::FluidStateExt as _;
267 use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
268
269 use super::*;
270
271 fn same_water(candidate: FluidState) -> bool {
272 candidate.is_water()
273 }
274
275 #[test]
276 fn vanilla_fluid_states_resolve_to_their_liquid_block_at_every_level() {
277 init_vanilla_registry();
278 init_behaviors();
279
280 let cases = [
281 (
282 &vanilla_fluids::WATER,
283 &vanilla_fluids::FLOWING_WATER,
284 &vanilla_blocks::WATER,
285 ),
286 (
287 &vanilla_fluids::LAVA,
288 &vanilla_fluids::FLOWING_LAVA,
289 &vanilla_blocks::LAVA,
290 ),
291 ];
292 let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
293 for (source, flowing, block) in cases {
294 let base = REGISTRY.blocks.get_default_state_id(block);
295 let mut states = vec![
296 (FluidState::source(source), 0),
297 (FluidState::flowing(flowing, 8, true), 8),
298 ];
299 states.extend(
300 (1..=7).map(|amount| (FluidState::flowing(flowing, amount, false), 8 - amount)),
301 );
302 for (fluid_state, level) in states {
303 let expected = base.set_value(&BlockStateProperties::LEVEL, level);
304 assert_eq!(
305 fluid_state_to_block(fluid_state),
306 expected,
307 "{fluid_state:?}"
308 );
309 assert_eq!(
310 fluid_state_to_block_with_existing(fluid_state, stone),
311 expected,
312 "{fluid_state:?}"
313 );
314 }
315 }
316 assert_eq!(
317 fluid_state_to_block(FluidState::EMPTY),
318 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
319 );
320 }
321
322 #[test]
323 fn height_treats_source_and_flowing_variants_as_same_fluid_above() {
324 init_vanilla_registry();
325
326 assert_eq!(
327 get_height_with(
328 FluidState::source(&vanilla_fluids::WATER),
329 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 4, false),
330 same_water,
331 )
332 .to_bits(),
333 1.0_f32.to_bits()
334 );
335 assert_eq!(
336 get_height_with(
337 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 4, false),
338 FluidState::source(&vanilla_fluids::WATER),
339 same_water,
340 )
341 .to_bits(),
342 1.0_f32.to_bits()
343 );
344 }
345
346 #[test]
347 fn empty_fluid_height_is_zero() {
348 init_vanilla_registry();
349
350 assert_eq!(
351 get_height_with(
352 FluidState::EMPTY,
353 FluidState::source(&vanilla_fluids::WATER),
354 same_water,
355 )
356 .to_bits(),
357 0.0_f32.to_bits()
358 );
359 }
360
361 #[test]
362 fn flow_points_toward_lower_same_fluid_neighbor() {
363 init_vanilla_registry();
364 let pos = BlockPos::new(0, 64, 0);
365 let flow = get_flow_with(
366 pos,
367 FluidState::source(&vanilla_fluids::WATER),
368 same_water,
369 |fluid_pos| {
370 if fluid_pos == pos.east() {
371 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 4, false)
372 } else {
373 FluidState::EMPTY
374 }
375 },
376 |_block_pos| vanilla_blocks::AIR.default_state(),
377 );
378
379 assert!((flow.x - 1.0).abs() < f64::EPSILON);
380 assert!(flow.y.abs() < f64::EPSILON);
381 assert!(flow.z.abs() < f64::EPSILON);
382 }
383
384 #[test]
385 fn falling_flow_pulls_down_when_horizontal_neighbor_has_solid_face() {
386 init_vanilla_registry();
387 let pos = BlockPos::new(0, 64, 0);
388 let flow = get_flow_with(
389 pos,
390 FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 8, true),
391 same_water,
392 |_fluid_pos| FluidState::EMPTY,
393 |block_pos| {
394 if block_pos == pos.east() {
395 vanilla_blocks::STONE.default_state()
396 } else {
397 vanilla_blocks::AIR.default_state()
398 }
399 },
400 );
401
402 assert!(flow.x.abs() < f64::EPSILON);
403 assert!((flow.y + 1.0).abs() < f64::EPSILON);
404 assert!(flow.z.abs() < f64::EPSILON);
405 }
406}