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