Skip to main content

steel_core/fluid/
state.rs

1//! Fluid state <-> block state conversions.
2//!
3//! Responsible for deriving `FluidState` from `BlockState`
4//! and converting `FluidState` back into `BlockStateId`.
5
6use 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/// Gets the fluid state at a given position.
25///
26/// Derives `FluidState` from the block state.
27#[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/// Gets the fluid state from a raw `BlockStateId`.
34#[must_use]
35pub fn get_fluid_state_from_block(state: BlockStateId) -> FluidState {
36    state.get_fluid_state()
37}
38
39/// Converts a `FluidState` into a `BlockStateId`, preserving the identity of an existing block.
40///
41/// If `existing_state` is a waterloggable block, this sets or clears its WATERLOGGED
42/// property rather than replacing the block entirely. Otherwise it falls back to the
43/// fluid's own liquid block, or AIR for empty fluid.
44#[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 empty, and the existing block can be waterlogged, un-waterlog it.
52        // If it cannot be waterlogged, it becomes air.
53        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        // Waterlogging only accepts source water, not flowing water.
68        return existing_state.set_value(&BlockStateProperties::WATERLOGGED, true);
69    }
70
71    fluid_state_to_block(fluid_state)
72}
73
74/// The `block` liquid state at `fluid_state`'s level, for fluids that have their own liquid block.
75#[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/// Converts a `FluidState` into a `BlockStateId` directly without preserving any block.
83#[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/// Gets the water source fluid ref from the registry.
91#[must_use]
92pub fn water_id() -> FluidRef {
93    &vanilla_fluids::WATER
94}
95
96/// Gets the lava source fluid ref from the registry.
97#[must_use]
98pub fn lava_id() -> FluidRef {
99    &vanilla_fluids::LAVA
100}
101
102/// Returns the fluid's own height as a fraction of a full block.
103/// `amount / 9.0` — source blocks have `amount = 8`, giving `0.888..`.
104/// Flowing blocks range from `amount = 1` (thin) to `7` (tall).
105#[must_use]
106pub fn get_own_height(fluid_state: FluidState) -> f32 {
107    f32::from(fluid_state.amount) / 9.0
108}
109
110/// Returns the effective fluid height at a position, accounting for fluid above.
111/// If the same fluid type occupies the block directly above (`hasSameAbove`),
112/// the height is `1.0` (full block). Otherwise it is `get_own_height(fluid_state)`.
113#[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/// Computes the horizontal flow direction of a fluid, weighted by the height
143/// difference to same-fluid neighbors.
144#[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}