Skip to main content

steel_core/fluid/
collision.rs

1//! Fluid collision and passability logic.
2//!
3//! Equivalent to various collision checks in FlowingFluid.java.
4
5use std::sync::Arc;
6
7use crate::behavior::BlockStateBehaviorExt;
8use crate::behavior::{BLOCK_BEHAVIORS, FLUID_BEHAVIORS};
9use crate::physics::shapes::merged_offset_face_occludes;
10use crate::world::World;
11use steel_registry::blocks::BlockRef;
12use steel_registry::blocks::block_state_ext::BlockStateExt;
13use steel_registry::blocks::properties::Direction;
14use steel_registry::fluid::FluidRef;
15use steel_registry::vanilla_block_tags::BlockTag;
16use steel_registry::vanilla_blocks;
17use steel_utils::{BlockPos, BlockStateId};
18
19// TODO: Add occlusion cache for performance (vanilla uses 200-entry ThreadLocal LRU)
20
21/// Checks if fluid can pass through a wall between two positions.
22#[must_use]
23pub fn can_pass_through_wall(
24    world: &Arc<World>,
25    from: BlockPos,
26    from_state: BlockStateId,
27    to: BlockPos,
28    to_state: BlockStateId,
29    direction: Direction,
30) -> bool {
31    if !world.is_in_valid_bounds(to) {
32        return false;
33    }
34
35    let from_shape = from_state.get_collision_shape_at(from);
36    let to_shape = to_state.get_collision_shape_at(to);
37
38    !merged_offset_face_occludes(from_shape, to_shape, direction)
39}
40
41/// Checks if a block state can hold any fluid, without world access.
42///
43/// Vanilla equivalent: `FlowingFluid.canHoldAnyFluid(BlockState)`.
44/// Uses `blocksMotion()` check instead of `has_collision` — vanilla's
45/// `blocksMotion()` = `block != Cobweb && block != BambooSapling && isSolid()`.
46#[must_use]
47pub fn can_hold_any_fluid_state(state: BlockStateId) -> bool {
48    let block = state.get_block();
49
50    // Vanilla: block instanceof LiquidBlockContainer → true
51    if state.is_liquid_container() {
52        return true;
53    }
54
55    // Vanilla: state.blocksMotion() ? false : !(exclusion list)
56    if state.blocks_motion() {
57        return false;
58    }
59
60    // Non-solid blocks that still reject fluid.
61    !is_fluid_excluded_block(block)
62}
63
64/// Returns true if a block is in the vanilla fluid exclusion list.
65fn is_fluid_excluded_block(block: BlockRef) -> bool {
66    block == &vanilla_blocks::LADDER
67        || block == &vanilla_blocks::SUGAR_CANE
68        || block == &vanilla_blocks::BUBBLE_COLUMN
69        || block == &vanilla_blocks::NETHER_PORTAL
70        || block == &vanilla_blocks::END_PORTAL
71        || block == &vanilla_blocks::END_GATEWAY
72        || block == &vanilla_blocks::STRUCTURE_VOID
73        || block.has_tag(&BlockTag::SIGNS)
74        || block.has_tag(&BlockTag::DOORS)
75}
76
77/// Vanilla equivalent: `FlowingFluid.canHoldSpecificFluid(BlockGetter, BlockPos, BlockState, Fluid)`.
78///
79/// For `LiquidBlockContainer` blocks, delegates to `canPlaceLiquid(null, ...)`.
80/// For other blocks, always returns true.
81#[must_use]
82pub fn can_hold_specific_fluid(state: BlockStateId, fluid: FluidRef) -> bool {
83    if state.is_liquid_container() {
84        let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
85        behavior.can_place_liquid(state, fluid)
86    } else {
87        true
88    }
89}
90
91/// Vanilla equivalent: `FlowingFluid.canHoldFluid(BlockGetter, BlockPos, BlockState, Fluid)`.
92///
93/// Combined check: `canHoldAnyFluid(state) && canHoldSpecificFluid(state, fluid)`.
94#[must_use]
95pub fn can_hold_fluid(state: BlockStateId, fluid: FluidRef) -> bool {
96    can_hold_any_fluid_state(state) && can_hold_specific_fluid(state, fluid)
97}
98
99/// Core passability logic for horizontal fluid spread.
100///
101/// Vanilla equivalent: `!isSourceBlockOfThisType(testFluidState) && canHoldAnyFluid(testState)`.
102#[must_use]
103pub fn can_pass_horizontally_internal(state: BlockStateId, target_fluid_id: FluidRef) -> bool {
104    // Vanilla: !isSourceBlockOfThisType — reject same-type source blocks
105    let fluid_state = state.get_fluid_state();
106    if FLUID_BEHAVIORS
107        .get_behavior(target_fluid_id)
108        .is_same(fluid_state.fluid_id)
109        && fluid_state.is_source()
110    {
111        return false;
112    }
113
114    // Vanilla: canHoldAnyFluid
115    can_hold_any_fluid_state(state)
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::behavior::init_behaviors;
121    use steel_registry::blocks::properties::{BlockStateProperties, SlabType};
122    use steel_registry::{init_vanilla_registry, vanilla_blocks, vanilla_fluids};
123
124    use super::*;
125
126    #[test]
127    fn waterlogged_source_blocks_block_same_fluid_flowing_variant_spread() {
128        init_vanilla_registry();
129        init_behaviors();
130
131        let waterlogged = vanilla_blocks::OAK_LEAVES
132            .default_state()
133            .set_value(&BlockStateProperties::WATERLOGGED, true);
134
135        assert!(!can_pass_horizontally_internal(
136            waterlogged,
137            &vanilla_fluids::FLOWING_WATER
138        ));
139        assert!(!can_pass_horizontally_internal(
140            waterlogged,
141            &vanilla_fluids::WATER
142        ));
143    }
144
145    #[test]
146    fn dry_waterloggable_blocks_reject_flowing_water() {
147        init_vanilla_registry();
148        init_behaviors();
149
150        let dry = vanilla_blocks::OAK_SLAB
151            .default_state()
152            .set_value(&BlockStateProperties::SLAB_TYPE, SlabType::Bottom)
153            .set_value(&BlockStateProperties::WATERLOGGED, false);
154
155        assert!(can_hold_specific_fluid(dry, &vanilla_fluids::WATER));
156        assert!(!can_hold_specific_fluid(
157            dry,
158            &vanilla_fluids::FLOWING_WATER
159        ));
160    }
161
162    #[test]
163    fn kelp_is_liquid_container_without_waterlogged_property() {
164        init_vanilla_registry();
165        init_behaviors();
166
167        let kelp = vanilla_blocks::KELP.default_state();
168
169        assert!(
170            kelp.try_get_value(&BlockStateProperties::WATERLOGGED)
171                .is_none()
172        );
173        assert!(kelp.is_liquid_container());
174        assert!(can_hold_any_fluid_state(kelp));
175        assert!(!can_hold_specific_fluid(kelp, &vanilla_fluids::WATER));
176        assert!(!can_hold_specific_fluid(kelp, &vanilla_fluids::LAVA));
177    }
178}