Skip to main content

steel_registry/blocks/
block_state_ext.rs

1use crate::vanilla_blocks;
2use crate::{
3    REGISTRY,
4    blocks::{
5        self, BlockRef,
6        properties::{BlockStateProperties, Direction, Property},
7        shapes::SupportType,
8    },
9};
10use steel_utils::BlockStateId;
11
12pub trait BlockStateExt {
13    fn get_block(&self) -> BlockRef;
14    fn is_air(&self) -> bool;
15    fn has_block_entity(&self) -> bool;
16    fn get_value<T, P: Property<T>>(&self, property: &P) -> T;
17    /// Gets the value of a property, returning `None` if the block doesn't have this property.
18    fn try_get_value<T, P: Property<T>>(&self, property: &P) -> Option<T>;
19    #[must_use]
20    fn set_value<T, P: Property<T>>(&self, property: &P, value: T) -> BlockStateId;
21    fn get_property_str(&self, name: &str) -> Option<String>;
22    fn get_collision_shape(&self) -> blocks::shapes::VoxelShape;
23    fn get_support_shape(&self) -> blocks::shapes::VoxelShape;
24    fn get_outline_shape(&self) -> blocks::shapes::VoxelShape;
25    fn get_occlusion_shape(&self) -> blocks::shapes::VoxelShape;
26    fn get_interaction_shape(&self) -> blocks::shapes::VoxelShape;
27    fn get_visual_shape(&self) -> blocks::shapes::VoxelShape;
28    /// Checks if this block face is sturdy enough to support other blocks.
29    /// Uses `SupportType::Full` by default.
30    fn is_face_sturdy(&self, direction: Direction) -> bool;
31    /// Checks if this block face is sturdy for the given support type.
32    fn is_face_sturdy_for(&self, direction: Direction, support_type: SupportType) -> bool;
33    /// Checks if this block state is solid (has a full cube collision shape).
34    ///
35    /// This matches vanilla's `BlockState.isSolid()` which is used by standing signs
36    /// to check if they can be placed on a block.
37    fn is_solid(&self) -> bool;
38    /// Checks if this block state blocks motion.
39    ///
40    /// This matches vanilla's `BlockState.blocksMotion()`.
41    fn blocks_motion(&self) -> bool;
42    /// Checks if this block state renders as a full solid cube.
43    ///
44    /// This matches vanilla's cached `BlockState.isSolidRender()`, based on the
45    /// occlusion shape rather than collision shape.
46    fn is_solid_render(&self) -> bool;
47    /// Returns if a block can be replaced extracted from the minecraft data
48    fn is_replaceable(&self) -> bool;
49    /// Returns true if this block state contains fluid — either a liquid block or a waterlogged block.
50    /// Mirrors vanilla's `!blockState.getFluidState().isEmpty()`.
51    fn has_fluid(&self) -> bool;
52}
53
54impl BlockStateExt for BlockStateId {
55    fn get_block(&self) -> BlockRef {
56        REGISTRY
57            .blocks
58            .by_state_id(*self)
59            .expect("Expected a valid state id")
60    }
61
62    fn is_air(&self) -> bool {
63        self.get_block().config.is_air
64    }
65
66    fn has_block_entity(&self) -> bool {
67        // TODO: Implement when block entities are added
68        false
69    }
70
71    fn get_value<T, P: Property<T>>(&self, property: &P) -> T {
72        REGISTRY.blocks.get_property(*self, property)
73    }
74
75    fn try_get_value<T, P: Property<T>>(&self, property: &P) -> Option<T> {
76        REGISTRY.blocks.try_get_property(*self, property)
77    }
78
79    fn set_value<T, P: Property<T>>(&self, property: &P, value: T) -> BlockStateId {
80        REGISTRY.blocks.set_property(*self, property, value)
81    }
82
83    fn get_property_str(&self, name: &str) -> Option<String> {
84        REGISTRY
85            .blocks
86            .get_properties(*self)
87            .into_iter()
88            .find(|(n, _)| *n == name)
89            .map(|(_, v)| v.to_string())
90    }
91
92    fn get_collision_shape(&self) -> blocks::shapes::VoxelShape {
93        REGISTRY.blocks.get_collision_shape(*self)
94    }
95
96    fn get_support_shape(&self) -> blocks::shapes::VoxelShape {
97        REGISTRY.blocks.get_support_shape(*self)
98    }
99
100    fn get_outline_shape(&self) -> blocks::shapes::VoxelShape {
101        REGISTRY.blocks.get_outline_shape(*self)
102    }
103
104    fn get_occlusion_shape(&self) -> blocks::shapes::VoxelShape {
105        REGISTRY.blocks.get_occlusion_shape(*self)
106    }
107
108    fn get_interaction_shape(&self) -> blocks::shapes::VoxelShape {
109        REGISTRY.blocks.get_interaction_shape(*self)
110    }
111
112    fn get_visual_shape(&self) -> blocks::shapes::VoxelShape {
113        REGISTRY.blocks.get_visual_shape(*self)
114    }
115
116    fn is_face_sturdy(&self, direction: Direction) -> bool {
117        self.is_face_sturdy_for(direction, SupportType::Full)
118    }
119
120    fn is_face_sturdy_for(&self, direction: Direction, support_type: SupportType) -> bool {
121        let shape = self.get_support_shape();
122        blocks::shapes::is_face_sturdy(shape, direction, support_type)
123    }
124
125    fn is_solid(&self) -> bool {
126        let block = self.get_block();
127
128        // Check force flags first (matches vanilla's calculateSolid)
129        if block.config.force_solid_on {
130            return true;
131        }
132        if block.config.force_solid_off {
133            return false;
134        }
135
136        // Vanilla's calculateSolid: check collision shape bounding box.
137        // A block is solid if its average dimension size >= 35/48 (~0.7292)
138        // or its Y size >= 1.0. This catches partial blocks like cactus
139        let shape = self.get_collision_shape();
140        if shape.is_empty() {
141            return false;
142        }
143        let bounds = blocks::shapes::bounding_box(shape);
144        bounds.size() >= 0.729_166_7 || bounds.height() >= 1.0
145    }
146
147    fn blocks_motion(&self) -> bool {
148        let block = self.get_block();
149        block != &vanilla_blocks::COBWEB
150            && block != &vanilla_blocks::BAMBOO_SAPLING
151            && self.is_solid()
152    }
153
154    fn is_solid_render(&self) -> bool {
155        self.get_block().config.can_occlude
156            && blocks::shapes::is_shape_full_block(self.get_occlusion_shape())
157    }
158
159    fn is_replaceable(&self) -> bool {
160        self.get_block().config.replaceable
161    }
162
163    fn has_fluid(&self) -> bool {
164        self.get_block().config.liquid
165            || self
166                .try_get_value(&BlockStateProperties::WATERLOGGED)
167                .unwrap_or(false)
168    }
169}
170
171pub trait FluidReplaceableExt {
172    fn can_be_replaced_by_fluid(&self, fluid: BlockRef) -> bool;
173}
174
175impl FluidReplaceableExt for BlockStateId {
176    fn can_be_replaced_by_fluid(&self, fluid: BlockRef) -> bool {
177        let block = self.get_block();
178
179        if block == &vanilla_blocks::AIR {
180            return true;
181        }
182
183        if fluid == &vanilla_blocks::WATER
184            && let Some(false) = self.try_get_value(&BlockStateProperties::WATERLOGGED)
185        {
186            return true;
187        }
188
189        // Vanilla: `state.canBeReplaced() || !state.isSolid()`
190        block.config.replaceable || !self.is_solid()
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::blocks::properties::BlockStateProperties;
198    use crate::blocks::shapes::SupportType;
199    use crate::test_support::init_test_registry;
200    use steel_utils::Direction;
201
202    #[test]
203    fn solid_render_uses_occlusion_shape_not_collision_shape() {
204        init_test_registry();
205
206        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
207        assert!(stone.is_solid_render());
208
209        let glass = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::GLASS);
210        assert!(blocks::shapes::is_shape_full_block(
211            glass.get_collision_shape()
212        ));
213        assert!(!glass.is_solid_render());
214    }
215
216    #[test]
217    fn blocks_motion_matches_vanilla_base_predicate() {
218        init_test_registry();
219
220        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
221        assert!(stone.blocks_motion());
222
223        let water = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
224        assert!(!water.blocks_motion());
225        assert!(water.has_fluid());
226
227        let cobweb = REGISTRY
228            .blocks
229            .get_default_state_id(&vanilla_blocks::COBWEB);
230        assert!(!cobweb.blocks_motion());
231    }
232
233    #[test]
234    fn fence_post_supports_center_attachments_from_below() {
235        init_test_registry();
236
237        let fence = vanilla_blocks::OAK_FENCE
238            .default_state()
239            .set_value(&BlockStateProperties::EAST, true);
240
241        assert!(fence.is_face_sturdy_for(Direction::Down, SupportType::Center));
242    }
243}