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 fn get_ticking_metadata(&self) -> BlockStateTickingMetadata;
20 fn get_fluid_state(&self) -> FluidState;
22 fn has_fluid(&self) -> bool;
24 fn is_randomly_ticking(&self) -> bool;
26 fn has_block_entity(&self) -> bool;
32 fn get_value<P: Property>(&self, property: &P) -> P::Value;
33 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 fn get_light_emission(&self) -> u8;
53 fn get_light_dampening(&self) -> u8;
55 fn use_shape_for_light_occlusion(&self) -> bool;
57 fn get_offset(&self, pos: BlockPos) -> DVec3;
58 fn is_face_sturdy_at(&self, pos: BlockPos, direction: Direction) -> bool;
61 fn is_face_sturdy_for_at(
63 &self,
64 pos: BlockPos,
65 direction: Direction,
66 support_type: SupportType,
67 ) -> bool;
68 fn is_solid(&self) -> bool;
73 fn blocks_motion(&self) -> bool;
77 fn is_solid_render(&self) -> bool;
82 fn is_suffocating(&self) -> bool;
83 fn is_static_redstone_conductor(&self) -> bool;
86 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 if block.config.force_solid_on {
233 return true;
234 }
235 if block.config.force_solid_off {
236 return false;
237 }
238
239 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}