Skip to main content

steel_registry/blocks/
block_state_ext.rs

1use crate::vanilla_blocks;
2use crate::{
3    REGISTRY,
4    blocks::{
5        self, BlockRef, BlockStateTickingMetadata,
6        properties::{Direction, Property},
7        shapes::{OffsetVoxelShape, SupportType},
8    },
9    fluid::FluidState,
10};
11use glam::DVec3;
12use steel_utils::BlockPos;
13use steel_utils::BlockStateId;
14
15pub trait BlockStateExt {
16    fn get_block(&self) -> BlockRef;
17    fn is_air(&self) -> bool;
18    /// Returns Vanilla's immutable cached fluid/random-tick metadata for this state.
19    fn get_ticking_metadata(&self) -> BlockStateTickingMetadata;
20    /// Mirrors Vanilla's cached `BlockState.getFluidState()`.
21    fn get_fluid_state(&self) -> FluidState;
22    /// Returns whether the cached fluid state is non-empty.
23    fn has_fluid(&self) -> bool;
24    /// Mirrors Vanilla's cached `BlockState.isRandomlyTicking()` for the block callback.
25    fn is_randomly_ticking(&self) -> bool;
26    /// Returns whether this block structurally supports a block entity.
27    ///
28    /// Extracted Vanilla type memberships match `EntityBlock` exactly. Steel extends that into a
29    /// registration contract: plugin blocks must be accepted by at least one registered block
30    /// entity type, while the owning block behavior remains responsible for instance creation.
31    fn has_block_entity(&self) -> bool;
32    fn get_value<P: Property>(&self, property: &P) -> P::Value;
33    /// Gets the value of a property, returning `None` if the block doesn't have this property.
34    fn try_get_value<P: Property>(&self, property: &P) -> Option<P::Value>;
35    #[must_use]
36    fn set_value<P: Property>(&self, property: &P, value: P::Value) -> BlockStateId;
37    fn copy_value<P: Property, O: BlockStateExt>(&self, property: &P, other: &O) -> BlockStateId;
38    fn get_property_str(&self, name: &str) -> Option<&'static str>;
39    fn with_properties_of(&self, source: BlockStateId) -> BlockStateId;
40    fn get_static_collision_shape(&self) -> blocks::shapes::VoxelShape;
41    fn get_collision_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape;
42    fn get_static_support_shape(&self) -> blocks::shapes::VoxelShape;
43    fn get_support_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape;
44    fn get_static_outline_shape(&self) -> blocks::shapes::VoxelShape;
45    fn get_outline_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape;
46    fn get_occlusion_shape(&self) -> blocks::shapes::VoxelShape;
47    fn get_static_interaction_shape(&self) -> blocks::shapes::VoxelShape;
48    fn get_interaction_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape;
49    fn get_static_visual_shape(&self) -> blocks::shapes::VoxelShape;
50    fn get_visual_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape;
51    /// Returns this block state's block light emission, in vanilla's 0-15 range.
52    fn get_light_emission(&self) -> u8;
53    /// Returns this block state's light dampening, in vanilla's 0-15 range.
54    fn get_light_dampening(&self) -> u8;
55    /// Returns true if vanilla uses face shapes for light occlusion on this state.
56    fn use_shape_for_light_occlusion(&self) -> bool;
57    fn get_offset(&self, pos: BlockPos) -> DVec3;
58    /// Checks if this block face is sturdy enough to support other blocks.
59    /// Uses `SupportType::Full` by default.
60    fn is_face_sturdy_at(&self, pos: BlockPos, direction: Direction) -> bool;
61    /// Checks if this block face is sturdy for the given support type.
62    fn is_face_sturdy_for_at(
63        &self,
64        pos: BlockPos,
65        direction: Direction,
66        support_type: SupportType,
67    ) -> bool;
68    /// Checks if this block state is solid (has a full cube collision shape).
69    ///
70    /// This matches vanilla's `BlockState.isSolid()` which is used by standing signs
71    /// to check if they can be placed on a block.
72    fn is_solid(&self) -> bool;
73    /// Checks if this block state blocks motion.
74    ///
75    /// This matches vanilla's `BlockState.blocksMotion()`.
76    fn blocks_motion(&self) -> bool;
77    /// Checks if this block state renders as a full solid cube.
78    ///
79    /// This matches vanilla's cached `BlockState.isSolidRender()`, based on the
80    /// occlusion shape rather than collision shape.
81    fn is_solid_render(&self) -> bool;
82    fn is_suffocating(&self) -> bool;
83    /// Returns the extracted static `BlockState.isRedstoneConductor` value.
84    /// Dynamic behavior queries must also receive the live level and position.
85    fn is_static_redstone_conductor(&self) -> bool;
86    /// Returns if a block can be replaced extracted from the minecraft data
87    fn is_replaceable(&self) -> bool;
88}
89
90impl BlockStateExt for BlockStateId {
91    fn get_block(&self) -> BlockRef {
92        REGISTRY
93            .blocks
94            .by_state_id(*self)
95            .expect("Expected a valid state id")
96    }
97
98    fn with_properties_of(&self, source: BlockStateId) -> BlockStateId {
99        REGISTRY.blocks.with_properties_of(*self, source)
100    }
101
102    fn is_air(&self) -> bool {
103        self.get_ticking_metadata().is_air()
104    }
105
106    fn get_ticking_metadata(&self) -> BlockStateTickingMetadata {
107        let Some(metadata) = REGISTRY.blocks.get_ticking_metadata(*self) else {
108            panic!("invalid block state id {}", self.0);
109        };
110        metadata
111    }
112
113    fn get_fluid_state(&self) -> FluidState {
114        self.get_ticking_metadata().fluid_state()
115    }
116
117    fn has_fluid(&self) -> bool {
118        self.get_ticking_metadata().has_fluid()
119    }
120
121    fn is_randomly_ticking(&self) -> bool {
122        self.get_ticking_metadata().randomly_ticking_block()
123    }
124
125    fn has_block_entity(&self) -> bool {
126        REGISTRY
127            .block_entity_types
128            .has_block_entity(self.get_block())
129    }
130
131    fn get_value<P: Property>(&self, property: &P) -> P::Value {
132        REGISTRY.blocks.get_property(*self, property)
133    }
134
135    fn try_get_value<P: Property>(&self, property: &P) -> Option<P::Value> {
136        REGISTRY.blocks.try_get_property(*self, property)
137    }
138
139    fn set_value<P: Property>(&self, property: &P, value: P::Value) -> BlockStateId {
140        REGISTRY.blocks.set_property(*self, property, value)
141    }
142
143    fn copy_value<P: Property, O: BlockStateExt>(&self, property: &P, other: &O) -> BlockStateId {
144        self.set_value(property, other.get_value(property))
145    }
146
147    fn get_property_str(&self, name: &str) -> Option<&'static str> {
148        REGISTRY.blocks.get_property_str(*self, name)
149    }
150
151    fn get_static_collision_shape(&self) -> blocks::shapes::VoxelShape {
152        REGISTRY.blocks.get_static_collision_shape(*self)
153    }
154
155    fn get_collision_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape {
156        REGISTRY.blocks.get_collision_shape_at(*self, pos)
157    }
158
159    fn get_static_support_shape(&self) -> blocks::shapes::VoxelShape {
160        REGISTRY.blocks.get_static_support_shape(*self)
161    }
162
163    fn get_support_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape {
164        REGISTRY.blocks.get_support_shape_at(*self, pos)
165    }
166
167    fn get_static_outline_shape(&self) -> blocks::shapes::VoxelShape {
168        REGISTRY.blocks.get_static_outline_shape(*self)
169    }
170
171    fn get_outline_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape {
172        REGISTRY.blocks.get_outline_shape_at(*self, pos)
173    }
174
175    fn get_occlusion_shape(&self) -> blocks::shapes::VoxelShape {
176        REGISTRY.blocks.get_occlusion_shape(*self)
177    }
178
179    fn get_static_interaction_shape(&self) -> blocks::shapes::VoxelShape {
180        REGISTRY.blocks.get_static_interaction_shape(*self)
181    }
182
183    fn get_interaction_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape {
184        REGISTRY.blocks.get_interaction_shape_at(*self, pos)
185    }
186
187    fn get_static_visual_shape(&self) -> blocks::shapes::VoxelShape {
188        REGISTRY.blocks.get_static_visual_shape(*self)
189    }
190
191    fn get_visual_shape_at(&self, pos: BlockPos) -> OffsetVoxelShape {
192        REGISTRY.blocks.get_visual_shape_at(*self, pos)
193    }
194
195    fn get_light_emission(&self) -> u8 {
196        REGISTRY.blocks.get_light_properties(*self).light_emission
197    }
198
199    fn get_light_dampening(&self) -> u8 {
200        REGISTRY.blocks.get_light_properties(*self).light_dampening
201    }
202
203    fn use_shape_for_light_occlusion(&self) -> bool {
204        REGISTRY
205            .blocks
206            .get_light_properties(*self)
207            .use_shape_for_light_occlusion
208    }
209
210    fn get_offset(&self, pos: BlockPos) -> DVec3 {
211        self.get_block().offset_at(pos)
212    }
213
214    fn is_face_sturdy_at(&self, pos: BlockPos, direction: Direction) -> bool {
215        self.is_face_sturdy_for_at(pos, direction, SupportType::Full)
216    }
217
218    fn is_face_sturdy_for_at(
219        &self,
220        pos: BlockPos,
221        direction: Direction,
222        support_type: SupportType,
223    ) -> bool {
224        let shape = self.get_support_shape_at(pos);
225        blocks::shapes::is_offset_face_sturdy(shape, direction, support_type)
226    }
227
228    fn is_solid(&self) -> bool {
229        let block = self.get_block();
230
231        // Check force flags first (matches vanilla's calculateSolid)
232        if block.config.force_solid_on {
233            return true;
234        }
235        if block.config.force_solid_off {
236            return false;
237        }
238
239        // Vanilla's calculateSolid: check collision shape bounding box.
240        // A block is solid if its average dimension size >= 35/48 (~0.7292)
241        // or its Y size >= 1.0. This catches partial blocks like cactus
242        let shape = self.get_static_collision_shape();
243        if shape.is_empty() {
244            return false;
245        }
246        let bounds = blocks::shapes::bounding_box(shape);
247        bounds.size() >= 0.729_166_7 || bounds.height() >= 1.0
248    }
249
250    fn blocks_motion(&self) -> bool {
251        let block = self.get_block();
252        block != &vanilla_blocks::COBWEB
253            && block != &vanilla_blocks::BAMBOO_SAPLING
254            && self.is_solid()
255    }
256
257    fn is_solid_render(&self) -> bool {
258        self.get_block().config.can_occlude
259            && blocks::shapes::is_shape_full_block(self.get_occlusion_shape())
260    }
261
262    fn is_suffocating(&self) -> bool {
263        REGISTRY.blocks.is_suffocating(*self)
264    }
265
266    fn is_static_redstone_conductor(&self) -> bool {
267        REGISTRY.blocks.is_static_redstone_conductor(*self)
268    }
269
270    fn is_replaceable(&self) -> bool {
271        self.get_block().config.replaceable
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use crate::blocks::behavior::OffsetType;
279    use crate::blocks::properties::BlockStateProperties;
280    use crate::blocks::shapes::{ShapeChannel, SupportType};
281    use crate::{init_vanilla_registry, vanilla_fluids};
282    use steel_utils::Direction;
283
284    #[test]
285    fn solid_render_uses_occlusion_shape_not_collision_shape() {
286        init_vanilla_registry();
287
288        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
289        assert!(stone.is_solid_render());
290
291        let glass = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::GLASS);
292        assert!(blocks::shapes::is_shape_full_block(
293            glass.get_static_collision_shape()
294        ));
295        assert!(!glass.is_solid_render());
296    }
297
298    #[test]
299    fn light_properties_match_generated_state_offsets() {
300        init_vanilla_registry();
301
302        let air = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR);
303        assert_eq!(air.get_light_emission(), 0);
304        assert_eq!(air.get_light_dampening(), 0);
305        assert!(!air.use_shape_for_light_occlusion());
306
307        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
308        assert_eq!(stone.get_light_emission(), 0);
309        assert_eq!(stone.get_light_dampening(), 15);
310        assert!(!stone.use_shape_for_light_occlusion());
311
312        let light = vanilla_blocks::LIGHT.default_state();
313        assert_eq!(light.get_light_emission(), 15);
314        let dim_light = light.set_value(&BlockStateProperties::LEVEL, 7);
315        assert_eq!(dim_light.get_light_emission(), 7);
316
317        let sticky_piston = vanilla_blocks::STICKY_PISTON.default_state();
318        assert_eq!(sticky_piston.get_light_dampening(), 15);
319        assert!(!sticky_piston.use_shape_for_light_occlusion());
320
321        let extended_piston = sticky_piston.set_value(&BlockStateProperties::EXTENDED, true);
322        assert_eq!(extended_piston.get_light_dampening(), 0);
323        assert!(extended_piston.use_shape_for_light_occlusion());
324    }
325
326    #[test]
327    fn blocks_motion_matches_vanilla_base_predicate() {
328        init_vanilla_registry();
329
330        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
331        assert!(stone.blocks_motion());
332
333        let water = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::WATER);
334        assert!(!water.blocks_motion());
335
336        let cobweb = REGISTRY
337            .blocks
338            .get_default_state_id(&vanilla_blocks::COBWEB);
339        assert!(!cobweb.blocks_motion());
340    }
341
342    #[test]
343    fn suffocating_uses_extracted_vanilla_state_predicate() {
344        init_vanilla_registry();
345
346        let stone = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::STONE);
347        assert!(stone.is_suffocating());
348
349        let glass = REGISTRY.blocks.get_default_state_id(&vanilla_blocks::GLASS);
350        assert!(glass.blocks_motion());
351        assert!(!glass.is_suffocating());
352
353        let farmland = REGISTRY
354            .blocks
355            .get_default_state_id(&vanilla_blocks::FARMLAND);
356        assert!(farmland.is_suffocating());
357    }
358
359    #[test]
360    fn static_redstone_conductor_uses_extracted_vanilla_state_predicate() {
361        init_vanilla_registry();
362
363        assert!(
364            vanilla_blocks::STONE
365                .default_state()
366                .is_static_redstone_conductor()
367        );
368        assert!(
369            vanilla_blocks::SOUL_SAND
370                .default_state()
371                .is_static_redstone_conductor()
372        );
373        assert!(
374            !vanilla_blocks::REDSTONE_BLOCK
375                .default_state()
376                .is_static_redstone_conductor()
377        );
378        assert!(
379            !vanilla_blocks::PISTON
380                .default_state()
381                .is_static_redstone_conductor()
382        );
383    }
384
385    #[test]
386    fn vanilla_air_variants_are_air() {
387        init_vanilla_registry();
388
389        assert!(vanilla_blocks::AIR.default_state().is_air());
390        assert!(vanilla_blocks::CAVE_AIR.default_state().is_air());
391        assert!(vanilla_blocks::VOID_AIR.default_state().is_air());
392    }
393
394    #[test]
395    fn block_entity_presence_uses_extracted_type_validity() {
396        init_vanilla_registry();
397
398        assert!(
399            vanilla_blocks::MOVING_PISTON
400                .default_state()
401                .has_block_entity()
402        );
403        assert!(vanilla_blocks::CHEST.default_state().has_block_entity());
404        assert!(!vanilla_blocks::STONE.default_state().has_block_entity());
405    }
406
407    #[test]
408    fn fence_post_supports_center_attachments_from_below() {
409        init_vanilla_registry();
410
411        let fence = vanilla_blocks::OAK_FENCE
412            .default_state()
413            .set_value(&BlockStateProperties::EAST, true);
414
415        assert!(fence.is_face_sturdy_for_at(BlockPos::ZERO, Direction::Down, SupportType::Center));
416    }
417
418    #[test]
419    fn generated_shape_offset_flags_distinguish_visual_offset_from_server_shapes() {
420        init_vanilla_registry();
421
422        let sulfur_spike = vanilla_blocks::SULFUR_SPIKE.default_state().get_block();
423        assert_eq!(sulfur_spike.config.offset_type, OffsetType::Xz);
424        assert_eq!(sulfur_spike.config.max_horizontal_offset, 0.125);
425        assert!(
426            sulfur_spike
427                .shape_offsets
428                .uses_offset(ShapeChannel::Collision)
429        );
430        assert!(
431            sulfur_spike
432                .shape_offsets
433                .uses_offset(ShapeChannel::Outline)
434        );
435
436        let tall_grass = vanilla_blocks::TALL_GRASS.default_state().get_block();
437        assert_eq!(tall_grass.config.offset_type, OffsetType::Xz);
438        assert!(
439            !tall_grass
440                .shape_offsets
441                .uses_offset(ShapeChannel::Collision)
442        );
443        assert!(!tall_grass.shape_offsets.uses_offset(ShapeChannel::Outline));
444    }
445
446    #[test]
447    fn with_properties_of_keeps_target_values_for_non_matching_properties() {
448        init_vanilla_registry();
449
450        let source = vanilla_blocks::STONE.default_state();
451        let target = vanilla_blocks::CANDLE
452            .default_state()
453            .set_value(&BlockStateProperties::CANDLES, 3)
454            .set_value(&BlockStateProperties::WATERLOGGED, true);
455
456        assert_eq!(target.with_properties_of(source), target);
457    }
458
459    #[test]
460    fn cached_random_tick_metadata_tracks_state_dependent_predicates() {
461        init_vanilla_registry();
462
463        let decaying_leaves = vanilla_blocks::OAK_LEAVES.default_state();
464        assert!(decaying_leaves.is_randomly_ticking());
465        assert!(
466            !decaying_leaves
467                .set_value(&BlockStateProperties::DISTANCE, 6)
468                .is_randomly_ticking()
469        );
470        assert!(
471            !decaying_leaves
472                .set_value(&BlockStateProperties::PERSISTENT, true)
473                .is_randomly_ticking()
474        );
475
476        let immature_wheat = vanilla_blocks::WHEAT.default_state();
477        assert!(immature_wheat.is_randomly_ticking());
478        assert!(
479            !immature_wheat
480                .set_value(&BlockStateProperties::AGE_7, 7)
481                .is_randomly_ticking()
482        );
483    }
484
485    #[test]
486    fn cached_fluid_state_preserves_source_flowing_and_falling_variants() {
487        init_vanilla_registry();
488
489        let wet_slab = vanilla_blocks::OAK_SLAB
490            .default_state()
491            .set_value(&BlockStateProperties::WATERLOGGED, true);
492        assert_eq!(
493            wet_slab.get_fluid_state(),
494            FluidState::source(&vanilla_fluids::WATER)
495        );
496
497        let wet_grate = vanilla_blocks::COPPER_GRATE
498            .default_state()
499            .set_value(&BlockStateProperties::WATERLOGGED, true);
500        assert_eq!(
501            wet_grate.get_fluid_state(),
502            FluidState::new(&vanilla_fluids::WATER, 8, true)
503        );
504
505        let source_water = vanilla_blocks::WATER.default_state();
506        assert_eq!(
507            source_water.get_fluid_state(),
508            FluidState::source(&vanilla_fluids::WATER)
509        );
510        assert_eq!(
511            source_water
512                .set_value(&BlockStateProperties::LEVEL, 1)
513                .get_fluid_state(),
514            FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 7, false)
515        );
516        assert_eq!(
517            source_water
518                .set_value(&BlockStateProperties::LEVEL, 8)
519                .get_fluid_state(),
520            FluidState::flowing(&vanilla_fluids::FLOWING_WATER, 8, true)
521        );
522    }
523
524    #[test]
525    fn cached_metadata_keeps_block_and_fluid_random_ticks_distinct() {
526        init_vanilla_registry();
527
528        let lava = vanilla_blocks::LAVA.default_state().get_ticking_metadata();
529        assert!(lava.randomly_ticking_block());
530        assert!(lava.randomly_ticking_fluid());
531
532        let water = vanilla_blocks::WATER.default_state().get_ticking_metadata();
533        assert!(!water.randomly_ticking_block());
534        assert!(!water.randomly_ticking_fluid());
535    }
536}