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