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::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/// Gets the fluid state at a given position.
24///
25/// Derives `FluidState` from the block state.
26#[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/// Gets the fluid state from a raw `BlockStateId`.
33#[must_use]
34pub fn get_fluid_state_from_block(state: BlockStateId) -> FluidState {
35    state.get_fluid_state()
36}
37
38/// Converts a `FluidState` into a `BlockStateId`, preserving the identity of an existing block.
39///
40/// If `existing_state` is a waterloggable block, this sets or clears its WATERLOGGED
41/// property rather than replacing the block entirely. Otherwise it falls back to the
42/// raw fluid block (WATER/LAVA) or AIR for empty fluid.
43#[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 empty, and the existing block can be waterlogged, un-waterlog it.
51        // If it cannot be waterlogged, it becomes air.
52        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            // Vanilla SimpleWaterloggedBlock only accepts source WATER, not FLOWING_WATER.
69            return existing_state.set_value(&BlockStateProperties::WATERLOGGED, true);
70        }
71
72        // If not waterloggable, fall back to pure water block
73        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    // Unknown fluid type - default to air
85    REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
86}
87
88/// Converts a `FluidState` into a `BlockStateId` directly without preserving any block.
89///
90/// Handles LEVEL property mapping.
91#[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        // Use FluidState's to_block_level method for proper conversion
99        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        // Unknown fluid type - default to air
107        REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
108    }
109}
110
111/// Gets the water source fluid ref from the registry.
112#[must_use]
113pub fn water_id() -> FluidRef {
114    &vanilla_fluids::WATER
115}
116
117/// Gets the lava source fluid ref from the registry.
118#[must_use]
119pub fn lava_id() -> FluidRef {
120    &vanilla_fluids::LAVA
121}
122
123/// Returns the fluid's own height as a fraction of a full block.
124/// `amount / 9.0` — source blocks have `amount = 8`, giving `0.888..`.
125/// Flowing blocks range from `amount = 1` (thin) to `7` (tall).
126#[must_use]
127pub fn get_own_height(fluid_state: FluidState) -> f32 {
128    f32::from(fluid_state.amount) / 9.0
129}
130
131/// Returns the effective fluid height at a position, accounting for fluid above.
132/// If the same fluid type occupies the block directly above (`hasSameAbove`),
133/// the height is `1.0` (full block). Otherwise it is `get_own_height(fluid_state)`.
134#[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/// Returns vanilla `FlowingFluid.getFlow` for this fluid state.
164#[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}