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<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 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;
59 fn is_face_sturdy_at(&self, pos: BlockPos, direction: Direction) -> bool;
62 fn is_face_sturdy_for_at(
64 &self,
65 pos: BlockPos,
66 direction: Direction,
67 support_type: SupportType,
68 ) -> bool;
69 fn is_solid(&self) -> bool;
74 fn blocks_motion(&self) -> bool;
78 fn is_solid_render(&self) -> bool;
83 fn is_suffocating(&self) -> bool;
85 fn is_static_redstone_conductor(&self) -> bool;
88 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 if block.config.force_solid_on {
240 return true;
241 }
242 if block.config.force_solid_off {
243 return false;
244 }
245
246 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}