Skip to main content

steel_registry/blocks/
mod.rs

1pub mod behavior;
2pub mod block_state_ext;
3pub mod properties;
4pub mod shapes;
5
6use std::sync::OnceLock;
7
8use rustc_hash::FxHashMap;
9
10use crate::blocks::behavior::BlockConfig;
11use crate::blocks::properties::{DynProperty, Property};
12use crate::{RegistryExt, TaggedRegistryExt};
13
14/// Function type for shape lookups. Takes a state offset and returns the shape.
15pub type ShapeFn = fn(u16) -> shapes::VoxelShape;
16
17pub struct Block {
18    pub key: Identifier,
19    pub config: BlockConfig,
20    pub properties: &'static [&'static dyn DynProperty],
21    pub default_state_offset: u16,
22    /// Function to get collision shape for a state offset
23    pub collision_shape: ShapeFn,
24    /// Function to get block support shape for a state offset
25    pub support_shape: ShapeFn,
26    /// Function to get outline shape for a state offset
27    pub outline_shape: ShapeFn,
28    /// Function to get occlusion shape for a state offset
29    pub occlusion_shape: ShapeFn,
30    /// Function to get interaction shape for a state offset
31    pub interaction_shape: ShapeFn,
32    /// Function to get visual shape for a state offset
33    pub visual_shape: ShapeFn,
34    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
35    pub id: OnceLock<usize>,
36}
37
38impl std::fmt::Debug for Block {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("Block")
41            .field("key", &self.key)
42            .field("config", &self.config)
43            .field("properties", &self.properties)
44            .field("default_state_offset", &self.default_state_offset)
45            .finish_non_exhaustive()
46    }
47}
48
49/// Default shape function that returns a full block.
50const fn full_block_shape(_offset: u16) -> shapes::VoxelShape {
51    shapes::VoxelShape::FULL_BLOCK
52}
53
54/// Default interaction shape function that returns an empty shape.
55const fn empty_shape(_offset: u16) -> shapes::VoxelShape {
56    shapes::VoxelShape::EMPTY
57}
58
59impl Block {
60    pub const fn new(
61        key: Identifier,
62        config: BlockConfig,
63        properties: &'static [&'static dyn DynProperty],
64    ) -> Self {
65        Self {
66            key,
67            config,
68            properties,
69            default_state_offset: 0,
70            collision_shape: full_block_shape,
71            support_shape: full_block_shape,
72            outline_shape: full_block_shape,
73            occlusion_shape: full_block_shape,
74            interaction_shape: empty_shape,
75            visual_shape: full_block_shape,
76            id: OnceLock::new(),
77        }
78    }
79
80    /// Sets the shape functions for this block.
81    pub const fn with_shapes(
82        mut self,
83        collision: ShapeFn,
84        support: ShapeFn,
85        outline: ShapeFn,
86        occlusion: ShapeFn,
87        interaction: ShapeFn,
88        visual: ShapeFn,
89    ) -> Self {
90        self.collision_shape = collision;
91        self.support_shape = support;
92        self.outline_shape = outline;
93        self.occlusion_shape = occlusion;
94        self.interaction_shape = interaction;
95        self.visual_shape = visual;
96        self
97    }
98
99    /// Gets the collision shape for a given state offset.
100    #[inline]
101    pub fn get_collision_shape(&self, offset: u16) -> shapes::VoxelShape {
102        (self.collision_shape)(offset)
103    }
104
105    /// Gets the block support shape for a given state offset.
106    #[inline]
107    pub fn get_support_shape(&self, offset: u16) -> shapes::VoxelShape {
108        (self.support_shape)(offset)
109    }
110
111    /// Gets the outline shape for a given state offset.
112    #[inline]
113    pub fn get_outline_shape(&self, offset: u16) -> shapes::VoxelShape {
114        (self.outline_shape)(offset)
115    }
116
117    /// Gets the occlusion shape for a given state offset.
118    #[inline]
119    pub fn get_occlusion_shape(&self, offset: u16) -> shapes::VoxelShape {
120        (self.occlusion_shape)(offset)
121    }
122
123    /// Gets the interaction shape for a given state offset.
124    #[inline]
125    pub fn get_interaction_shape(&self, offset: u16) -> shapes::VoxelShape {
126        (self.interaction_shape)(offset)
127    }
128
129    /// Gets the visual shape for a given state offset.
130    #[inline]
131    pub fn get_visual_shape(&self, offset: u16) -> shapes::VoxelShape {
132        (self.visual_shape)(offset)
133    }
134
135    /// Sets the default state offset for this block.
136    /// The offset is relative to the block's base state ID.
137    ///
138    /// For easier usage, consider using `with_default_state_from_indices` or the
139    /// `default_state!` macro instead of calculating the offset manually.
140    ///
141    /// # Example
142    /// ```ignore
143    /// const REPEATER: Block = Block::new("repeater", props, &[...])
144    ///     .with_default_state(4);
145    /// ```
146    pub(crate) const fn with_default_state(mut self, offset: u16) -> Self {
147        self.default_state_offset = offset;
148
149        self
150    }
151
152    /// Const helper to calculate state offset from property indices and counts.
153    /// Properties are processed in reverse order to match Minecraft's encoding
154    /// (last property = inner loop with multiplier 1).
155    #[must_use]
156    pub const fn calculate_offset(property_indices: &[usize], property_counts: &[usize]) -> u16 {
157        let mut offset = 0u16;
158        let mut multiplier = 1u16;
159        let len = property_indices.len();
160
161        // Iterate in reverse order: last property first (inner loop)
162        let mut i = len;
163        while i > 0 {
164            i -= 1;
165            offset += property_indices[i] as u16 * multiplier;
166            multiplier *= property_counts[i] as u16;
167        }
168
169        offset
170    }
171
172    #[must_use]
173    pub fn default_state(&'static self) -> BlockStateId {
174        crate::REGISTRY.blocks.get_default_state_id(self)
175    }
176
177    /// Returns `true` if this block is tagged with the given tag.
178    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
179        crate::REGISTRY.blocks.is_in_tag(self, tag)
180    }
181}
182
183pub type BlockRef = &'static Block;
184
185impl PartialEq for BlockRef {
186    #[expect(clippy::disallowed_methods)] // This IS the PartialEq impl; ptr::eq is correct here
187    fn eq(&self, other: &Self) -> bool {
188        std::ptr::eq(*self, *other)
189    }
190}
191
192impl Eq for BlockRef {}
193
194// The central registry for all blocks.
195pub struct BlockRegistry {
196    blocks_by_id: Vec<BlockRef>,
197    blocks_by_key: FxHashMap<Identifier, usize>,
198    tags: FxHashMap<Identifier, Vec<Identifier>>,
199    allows_registering: bool,
200    pub state_to_block_lookup: Vec<BlockRef>,
201    /// Maps state IDs to block IDs (parallel to `state_to_block_lookup` for O(1) lookup)
202    pub state_to_block_id: Vec<usize>,
203    /// Maps block IDs to their base state ID
204    pub block_to_base_state: Vec<u16>,
205    /// The next state ID to be allocated
206    pub next_state_id: u16,
207}
208
209impl Default for BlockRegistry {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215impl BlockRegistry {
216    // Creates a new, empty registry.
217    #[must_use]
218    pub fn new() -> Self {
219        Self {
220            blocks_by_id: Vec::new(),
221            blocks_by_key: FxHashMap::default(),
222            tags: FxHashMap::default(),
223            allows_registering: true,
224            state_to_block_lookup: Vec::new(),
225            state_to_block_id: Vec::new(),
226            block_to_base_state: Vec::new(),
227            next_state_id: 0,
228        }
229    }
230
231    pub fn register(&mut self, block: BlockRef) -> usize {
232        assert!(
233            self.allows_registering,
234            "Cannot register blocks after the registry has been frozen"
235        );
236
237        let id = self.blocks_by_id.len();
238        let base_state_id = self.next_state_id;
239
240        let cached = block.id.get_or_init(|| id);
241        assert_eq!(*cached, id, "block registered with conflicting id");
242        self.blocks_by_key.insert(block.key.clone(), id);
243        self.blocks_by_id.push(block);
244        self.block_to_base_state.push(base_state_id);
245
246        let mut state_count = 1;
247        for property in block.properties {
248            state_count *= property.get_possible_values().len();
249        }
250
251        for _ in 0..state_count {
252            self.state_to_block_lookup.push(block);
253            self.state_to_block_id.push(id);
254        }
255
256        self.next_state_id += state_count as u16;
257
258        id
259    }
260
261    fn try_block_index(&self, block: BlockRef) -> Option<usize> {
262        if let Some(id) = block.id.get().copied()
263            && self
264                .blocks_by_id
265                .get(id)
266                .is_some_and(|registered| *registered == block)
267        {
268            return Some(id);
269        }
270
271        self.blocks_by_key.get(&block.key).copied()
272    }
273
274    fn block_index(&self, block: BlockRef) -> usize {
275        let Some(id) = self.try_block_index(block) else {
276            panic!("Block not found");
277        };
278        id
279    }
280
281    #[must_use]
282    pub fn get_base_state_id(&self, block: BlockRef) -> BlockStateId {
283        let id = self.block_index(block);
284        BlockStateId(self.block_to_base_state[id])
285    }
286
287    /// Gets the default state ID for a block (base state + default offset)
288    #[must_use]
289    pub fn get_default_state_id(&self, block: BlockRef) -> BlockStateId {
290        let id = self.block_index(block);
291        let base = self.block_to_base_state[id];
292        BlockStateId(base + block.default_state_offset)
293    }
294
295    #[must_use]
296    pub fn by_state_id(&self, state_id: BlockStateId) -> Option<BlockRef> {
297        self.state_to_block_lookup.get(state_id.0 as usize).copied()
298    }
299
300    #[must_use]
301    pub fn get_properties(&self, id: BlockStateId) -> Vec<(&'static str, &'static str)> {
302        let block = self.by_state_id(id).expect("Invalid state ID");
303
304        // If block has no properties, return empty vec
305        if block.properties.is_empty() {
306            return Vec::new();
307        }
308
309        // Get the base state ID for this block (O(1) lookup)
310        let block_id = self.state_to_block_id[id.0 as usize];
311        let base_state_id = self.block_to_base_state[block_id];
312
313        // Calculate the relative state index
314        let relative_index = id.0 - base_state_id;
315
316        Self::decode_property_indices(block, relative_index)
317            .into_iter()
318            .zip(block.properties)
319            .map(|(value_index, prop)| (prop.get_name(), prop.get_possible_values()[value_index]))
320            .collect()
321    }
322
323    /// Gets the state ID for a block with the given properties.
324    ///
325    /// Returns `None` if the block key is unknown or if any property name/value is invalid.
326    ///
327    /// Properties can be provided in any order. Missing properties will use the block's
328    /// default values (typically index 0 for each property).
329    #[must_use]
330    pub fn state_id_from_properties(
331        &self,
332        key: &Identifier,
333        properties: &[(&str, &str)],
334    ) -> Option<BlockStateId> {
335        let block = self.by_key(key)?;
336        self.state_id_from_block_properties(block, properties)
337    }
338
339    /// Gets the state ID for a block with the given properties.
340    ///
341    /// Returns `None` if the block is not registered or if any property
342    /// name/value is invalid.
343    #[must_use]
344    pub fn state_id_from_block_properties(
345        &self,
346        block: BlockRef,
347        properties: &[(&str, &str)],
348    ) -> Option<BlockStateId> {
349        let block_id = self.try_block_index(block)?;
350        let base_state_id = self.block_to_base_state[block_id];
351
352        let mut property_indices = vec![0usize; block.properties.len()];
353        Self::apply_property_overrides(block, &mut property_indices, properties.iter().copied())?;
354
355        Some(BlockStateId(
356            base_state_id + Self::encode_property_indices(block, &property_indices),
357        ))
358    }
359
360    /// Gets the state ID for a block by applying properties over that block's
361    /// registered default state.
362    ///
363    /// Returns `None` if the block is not registered or if any property
364    /// name/value is invalid.
365    #[must_use]
366    pub fn state_id_from_block_defaulted_properties<'a>(
367        &self,
368        block: BlockRef,
369        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
370    ) -> Option<BlockStateId> {
371        let block_id = self.try_block_index(block)?;
372        let base_state_id = self.block_to_base_state[block_id];
373
374        let mut property_indices = Self::decode_property_indices(block, block.default_state_offset);
375        Self::apply_property_overrides(block, &mut property_indices, properties)?;
376
377        Some(BlockStateId(
378            base_state_id + Self::encode_property_indices(block, &property_indices),
379        ))
380    }
381
382    fn decode_property_indices(block: BlockRef, mut offset: u16) -> Vec<usize> {
383        let mut property_indices = vec![0; block.properties.len()];
384
385        for (i, prop) in block.properties.iter().enumerate().rev() {
386            let count = prop.get_possible_values().len() as u16;
387            property_indices[i] = (offset % count) as usize;
388            offset /= count;
389        }
390
391        property_indices
392    }
393
394    fn apply_property_overrides<'a>(
395        block: BlockRef,
396        property_indices: &mut [usize],
397        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
398    ) -> Option<()> {
399        for (prop_name, prop_value) in properties {
400            let prop_idx = block
401                .properties
402                .iter()
403                .position(|p| p.get_name() == prop_name)?;
404
405            let prop = block.properties[prop_idx];
406            let value_idx = prop
407                .get_possible_values()
408                .iter()
409                .position(|v| *v == prop_value)?;
410
411            property_indices[prop_idx] = value_idx;
412        }
413
414        Some(())
415    }
416
417    fn encode_property_indices(block: BlockRef, property_indices: &[usize]) -> u16 {
418        let mut offset = 0u16;
419        let mut multiplier = 1u16;
420        for (idx, prop) in property_indices.iter().zip(block.properties.iter()).rev() {
421            offset += *idx as u16 * multiplier;
422            multiplier *= prop.get_possible_values().len() as u16;
423        }
424
425        offset
426    }
427
428    // Panics if that property isn't supposed to be on this block.
429    pub fn get_property<T, P: Property<T>>(&self, id: BlockStateId, property: &P) -> T {
430        self.try_get_property(id, property)
431            .expect("Property not found on this block")
432    }
433
434    /// Gets the value of a property, returning `None` if the block doesn't have this property.
435    #[must_use]
436    pub fn try_get_property<T, P: Property<T>>(&self, id: BlockStateId, property: &P) -> Option<T> {
437        let block = self.by_state_id(id).expect("Invalid state ID");
438
439        // Find the property index in the block's property list
440        let property_index = block
441            .properties
442            .iter()
443            .position(|prop| prop.get_name() == property.as_dyn().get_name())?;
444
445        // Get the base state ID for this block (O(1) lookup)
446        let block_id = self.state_to_block_id[id.0 as usize];
447        let base_state_id = self.block_to_base_state[block_id];
448
449        // Calculate the relative state index
450        let relative_index = id.0 - base_state_id;
451
452        let property_indices = Self::decode_property_indices(block, relative_index);
453        let block_property = block.properties[property_index];
454        let block_values = block_property.get_possible_values();
455        let block_value = block_values[property_indices[property_index]];
456
457        property.get_value(block_value)
458    }
459
460    // Panics if that property isn't supposed to be on this block.
461    pub fn set_property<T, P: Property<T>>(
462        &self,
463        id: BlockStateId,
464        property: &P,
465        value: T,
466    ) -> BlockStateId {
467        let block = self.by_state_id(id).expect("Invalid state ID");
468
469        // Find the property index in the block's property list
470        let property_index = block
471            .properties
472            .iter()
473            .position(|prop| prop.get_name() == property.as_dyn().get_name())
474            .unwrap_or_else(|| {
475                panic!(
476                    "Property {} not found on block {}",
477                    property.as_dyn().get_name(),
478                    block.key
479                )
480            });
481
482        // Get the base state ID for this block (O(1) lookup)
483        let block_id = self.state_to_block_id[id.0 as usize];
484        let base_state_id = self.block_to_base_state[block_id];
485
486        // Calculate the relative state index
487        let relative_index = id.0 - base_state_id;
488
489        // Decode all property indices from the relative state index.
490        // Properties are decoded in reverse order (last property = inner loop).
491        let mut index = relative_index;
492        let mut property_indices = vec![0usize; block.properties.len()];
493
494        for (i, prop) in block.properties.iter().enumerate().rev() {
495            let count = prop.get_possible_values().len() as u16;
496            property_indices[i] = (index % count) as usize;
497            index /= count;
498        }
499
500        let caller_value_index = property.get_internal_index(&value);
501        let caller_values = property.as_dyn().get_possible_values();
502        let value_name = caller_values[caller_value_index];
503        let block_values = block.properties[property_index].get_possible_values();
504        let Some(new_value_index) = block_values.iter().position(|v| *v == value_name) else {
505            panic!(
506                "Value {} for property {} not found on block {}",
507                value_name,
508                property.as_dyn().get_name(),
509                block.key
510            );
511        };
512        property_indices[property_index] = new_value_index;
513
514        // Re-encode the property indices back to a state ID.
515        // Properties are processed in reverse order (last property = inner loop).
516        let mut new_relative_index = 0u16;
517        let mut multiplier = 1u16;
518        for (i, prop) in block.properties.iter().enumerate().rev() {
519            let count = prop.get_possible_values().len() as u16;
520            new_relative_index += property_indices[i] as u16 * multiplier;
521            multiplier *= count;
522        }
523
524        BlockStateId(base_state_id + new_relative_index)
525    }
526
527    pub fn iter(&self) -> impl Iterator<Item = (usize, BlockRef)> + '_ {
528        self.blocks_by_id
529            .iter()
530            .enumerate()
531            .map(|(id, &block)| (id, block))
532    }
533}
534
535crate::impl_registry_ext!(BlockRegistry, Block, blocks_by_id, blocks_by_key);
536
537impl crate::RegistryEntry for Block {
538    fn key(&self) -> &Identifier {
539        &self.key
540    }
541
542    fn try_id(&self) -> Option<usize> {
543        self.id.get().copied()
544    }
545}
546crate::impl_tagged_registry!(BlockRegistry, blocks_by_key, "block");
547
548// Shape lookup methods
549impl BlockRegistry {
550    fn shape_for_state(
551        &self,
552        state_id: BlockStateId,
553        shape: fn(&Block, u16) -> shapes::VoxelShape,
554    ) -> shapes::VoxelShape {
555        let block = self.state_to_block_lookup.get(state_id.0 as usize).copied();
556        let Some(block) = block else {
557            return shapes::VoxelShape::FULL_BLOCK;
558        };
559        let block_id = self
560            .state_to_block_id
561            .get(state_id.0 as usize)
562            .copied()
563            .unwrap_or(0);
564        let base_state = self.block_to_base_state.get(block_id).copied().unwrap_or(0);
565        let offset = state_id.0.saturating_sub(base_state);
566        shape(block, offset)
567    }
568
569    /// Gets the collision shape for a block state.
570    ///
571    /// For simple blocks this is typically a single full-block box.
572    /// For complex blocks like fences, this may be multiple boxes.
573    #[must_use]
574    pub fn get_collision_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
575        self.shape_for_state(state_id, Block::get_collision_shape)
576    }
577
578    /// Gets the block support shape for a block state.
579    ///
580    /// Vanilla support checks use `BlockState.getBlockSupportShape`, not collision shape,
581    /// for `isFaceSturdy` and multiface side attachment.
582    #[must_use]
583    pub fn get_support_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
584        self.shape_for_state(state_id, Block::get_support_shape)
585    }
586
587    /// Gets the outline shape for a block state.
588    ///
589    /// This is the shape shown when the player targets the block.
590    /// Often the same as collision shape, but can differ (e.g., fences).
591    #[must_use]
592    pub fn get_outline_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
593        self.shape_for_state(state_id, Block::get_outline_shape)
594    }
595
596    /// Gets the occlusion shape for a block state.
597    ///
598    /// Vanilla caches this as `BlockState.getOcclusionShape()` and uses it for
599    /// `isSolidRender`, light occlusion, and face occlusion.
600    #[must_use]
601    pub fn get_occlusion_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
602        self.shape_for_state(state_id, Block::get_occlusion_shape)
603    }
604
605    /// Gets the interaction shape for a block state.
606    ///
607    /// Vanilla uses this as an interaction hit override after the primary raycast
608    /// shape has already hit.
609    #[must_use]
610    pub fn get_interaction_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
611        self.shape_for_state(state_id, Block::get_interaction_shape)
612    }
613
614    /// Gets the visual shape for a block state.
615    ///
616    /// Vanilla uses this for visual raycasts; it defaults to collision shape but
617    /// differs for a few blocks such as fences, mud, soul sand, and powder snow.
618    #[must_use]
619    pub fn get_visual_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
620        self.shape_for_state(state_id, Block::get_visual_shape)
621    }
622
623    /// Gets all static shape channels for a block state.
624    #[must_use]
625    pub fn get_shapes(&self, state_id: BlockStateId) -> shapes::BlockShapes {
626        shapes::BlockShapes::new(
627            self.get_collision_shape(state_id),
628            self.get_support_shape(state_id),
629            self.get_outline_shape(state_id),
630            self.get_occlusion_shape(state_id),
631            self.get_interaction_shape(state_id),
632            self.get_visual_shape(state_id),
633        )
634    }
635
636    pub fn copy_matching_properties(&self, source: BlockStateId, target: BlockRef) -> BlockStateId {
637        let props = self.get_properties(source);
638        let matching: Vec<(&str, &str)> = props
639            .iter()
640            .filter(|(name, _)| target.properties.iter().any(|p| p.get_name() == *name))
641            .copied()
642            .collect();
643        self.state_id_from_block_properties(target, &matching)
644            .unwrap_or_else(|| self.get_default_state_id(target))
645    }
646}
647
648/// Macro to generate offset calculation from property values in all positions.
649///
650/// Takes property objects and their values, automatically converts to indices.
651/// All properties must be specified in order.
652///
653/// # Note
654/// For boolean properties, use `.index_of(value)` to handle the inverted encoding
655/// (true=0, false=1 for Java compatibility).
656///
657/// # Example
658/// ```ignore
659/// use steel_registry::{offset, properties::{BlockStateProperties as Props, RedstoneSide}};
660///
661/// const WIRE: Block = Block::new("wire", behavior, PROPS)
662///     .with_default_state(offset!(
663///         Props::EAST_REDSTONE => RedstoneSide::Up,
664///         Props::NORTH_REDSTONE => RedstoneSide::None,
665///         Props::POWER => 10,
666///         Props::ATTACHED => Props::ATTACHED.index_of(false)  // Bools need .index_of()
667///     ));
668/// ```
669#[macro_export]
670macro_rules! offset {
671    ($($prop:expr => $value:expr),* $(,)?) => {{
672        const INDICES: &[usize] = &[$($value as usize),*];
673        const COUNTS: &[usize] = &[$($prop.value_count()),*];
674        $crate::blocks::Block::calculate_offset(INDICES, COUNTS)
675    }};
676}
677
678/// Re-export for easier access
679pub use offset;
680use steel_utils::{BlockStateId, Identifier};
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use crate::blocks::properties::{BlockStateProperties, Direction};
686    use crate::vanilla_blocks;
687
688    fn create_test_registry() -> BlockRegistry {
689        let mut registry = BlockRegistry::new();
690        vanilla_blocks::register_blocks(&mut registry);
691        registry.freeze();
692        registry
693    }
694
695    #[test]
696    fn test_redstone_wire_properties() {
697        let registry = create_test_registry();
698        let redstone_wire = registry
699            .by_key(&Identifier::vanilla_static("redstone_wire"))
700            .expect("redstone_wire should exist");
701
702        // Redstone wire has 5 properties
703        assert_eq!(redstone_wire.properties.len(), 5);
704
705        // Check property names
706        let prop_names: Vec<&str> = redstone_wire
707            .properties
708            .iter()
709            .map(|p| p.get_name())
710            .collect();
711        assert!(prop_names.contains(&"east"));
712        assert!(prop_names.contains(&"north"));
713        assert!(prop_names.contains(&"south"));
714        assert!(prop_names.contains(&"west"));
715        assert!(prop_names.contains(&"power"));
716    }
717
718    #[test]
719    fn test_redstone_wire_state_count() {
720        let registry = create_test_registry();
721
722        // Redstone wire: 3 sides × 3 sides × 3 sides × 3 sides × 16 power levels = 1296 states
723        // Actually checking the state count
724        let redstone_wire = registry
725            .by_key(&Identifier::vanilla_static("redstone_wire"))
726            .expect("redstone_wire should exist");
727
728        let mut state_count = 1;
729        for prop in redstone_wire.properties {
730            state_count *= prop.get_possible_values().len();
731        }
732        assert_eq!(state_count, 3 * 3 * 3 * 3 * 16); // 1296
733    }
734
735    #[test]
736    fn test_get_properties_default_state() {
737        let registry = create_test_registry();
738        let redstone_wire = registry
739            .by_key(&Identifier::vanilla_static("redstone_wire"))
740            .expect("redstone_wire should exist");
741
742        let default_state = registry.get_default_state_id(redstone_wire);
743        let properties = registry.get_properties(default_state);
744
745        // Default state should have all sides "none" and power 0
746        assert_eq!(properties.len(), 5);
747
748        for (name, value) in &properties {
749            match *name {
750                "east" | "north" | "south" | "west" => {
751                    assert_eq!(*value, "none", "Default side should be 'none'");
752                }
753                "power" => {
754                    assert_eq!(*value, "0", "Default power should be '0'");
755                }
756                _ => panic!("Unexpected property: {}", name),
757            }
758        }
759    }
760
761    #[test]
762    fn test_state_id_from_properties_roundtrip() {
763        let registry = create_test_registry();
764        let key = Identifier::vanilla_static("redstone_wire");
765
766        // Test with specific properties
767        let properties = [
768            ("east", "up"),
769            ("north", "side"),
770            ("south", "none"),
771            ("west", "up"),
772            ("power", "15"),
773        ];
774
775        let state_id = registry
776            .state_id_from_properties(&key, &properties)
777            .expect("Should find state");
778
779        // Get properties back and verify
780        let retrieved = registry.get_properties(state_id);
781        assert_eq!(retrieved.len(), 5);
782
783        for (name, value) in &properties {
784            let found = retrieved
785                .iter()
786                .find(|(n, _)| n == name)
787                .expect("Property should exist");
788            assert_eq!(found.1, *value, "Property {} mismatch", name);
789        }
790    }
791
792    #[test]
793    fn test_state_id_from_properties_partial() {
794        let registry = create_test_registry();
795        let key = Identifier::vanilla_static("redstone_wire");
796
797        // Only specify some properties - others should default to index 0
798        let partial_props = [("power", "10"), ("east", "side")];
799
800        let state_id = registry
801            .state_id_from_properties(&key, &partial_props)
802            .expect("Should find state");
803
804        let retrieved = registry.get_properties(state_id);
805
806        // Verify specified properties
807        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
808        assert_eq!(power.1, "10");
809
810        let east = retrieved.iter().find(|(n, _)| *n == "east").unwrap();
811        assert_eq!(east.1, "side");
812
813        // Unspecified properties should be at index 0 (first value in enum)
814        let north = retrieved.iter().find(|(n, _)| *n == "north").unwrap();
815        assert_eq!(north.1, "up"); // Index 0 is "up" for RedstoneSide
816    }
817
818    #[test]
819    fn test_state_id_from_block_defaulted_properties_keeps_missing_defaults() {
820        let registry = create_test_registry();
821        let key = Identifier::vanilla_static("redstone_wire");
822        let block = registry.by_key(&key).expect("redstone_wire should exist");
823
824        let state_id = registry
825            .state_id_from_block_defaulted_properties(block, [("power", "10")])
826            .expect("Should find state");
827
828        let retrieved = registry.get_properties(state_id);
829
830        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
831        assert_eq!(power.1, "10");
832
833        for direction in ["east", "north", "south", "west"] {
834            let side = retrieved.iter().find(|(n, _)| *n == direction).unwrap();
835            assert_eq!(side.1, "none");
836        }
837    }
838
839    #[test]
840    fn test_state_id_from_properties_empty() {
841        let registry = create_test_registry();
842        let key = Identifier::vanilla_static("redstone_wire");
843
844        // Empty properties - should get base state with all defaults at index 0
845        let state_id = registry
846            .state_id_from_properties(&key, &[])
847            .expect("Should find state");
848
849        let retrieved = registry.get_properties(state_id);
850
851        // All should be at index 0
852        for (name, value) in &retrieved {
853            match *name {
854                "east" | "north" | "south" | "west" => {
855                    assert_eq!(*value, "up", "Empty props should use index 0 = 'up'");
856                }
857                "power" => {
858                    assert_eq!(*value, "0", "Empty props should use index 0 = '0'");
859                }
860                _ => {}
861            }
862        }
863    }
864
865    #[test]
866    fn test_state_id_from_properties_invalid_block() {
867        let registry = create_test_registry();
868        let key = Identifier::vanilla_static("nonexistent_block");
869
870        let result = registry.state_id_from_properties(&key, &[]);
871        assert!(result.is_none(), "Should return None for invalid block");
872    }
873
874    #[test]
875    fn test_state_id_from_properties_invalid_property() {
876        let registry = create_test_registry();
877        let key = Identifier::vanilla_static("redstone_wire");
878
879        let invalid_props = [("invalid_property", "value")];
880        let result = registry.state_id_from_properties(&key, &invalid_props);
881        assert!(result.is_none(), "Should return None for invalid property");
882    }
883
884    #[test]
885    fn test_state_id_from_properties_invalid_value() {
886        let registry = create_test_registry();
887        let key = Identifier::vanilla_static("redstone_wire");
888
889        let invalid_props = [("power", "999")]; // Power only goes 0-15
890        let result = registry.state_id_from_properties(&key, &invalid_props);
891        assert!(result.is_none(), "Should return None for invalid value");
892    }
893
894    #[test]
895    fn same_named_direction_properties_translate_by_value_name() {
896        let registry = create_test_registry();
897        let wall_torch = registry.get_default_state_id(&vanilla_blocks::WALL_TORCH);
898
899        let south_torch = registry.set_property(
900            wall_torch,
901            &BlockStateProperties::HORIZONTAL_FACING,
902            Direction::South,
903        );
904        let facing_from_six_way_property =
905            registry.try_get_property(south_torch, &BlockStateProperties::FACING);
906        assert_eq!(facing_from_six_way_property, Some(Direction::South));
907
908        let west_torch =
909            registry.set_property(south_torch, &BlockStateProperties::FACING, Direction::West);
910        let facing_from_horizontal_property =
911            registry.try_get_property(west_torch, &BlockStateProperties::HORIZONTAL_FACING);
912        assert_eq!(facing_from_horizontal_property, Some(Direction::West));
913
914        let dispenser = registry.get_default_state_id(&vanilla_blocks::DISPENSER);
915        let upward_dispenser =
916            registry.set_property(dispenser, &BlockStateProperties::FACING, Direction::Up);
917        let horizontal_facing =
918            registry.try_get_property(upward_dispenser, &BlockStateProperties::HORIZONTAL_FACING);
919        assert_eq!(horizontal_facing, None);
920    }
921
922    #[test]
923    fn test_state_id_from_properties_rejects_properties_on_propertyless_block() {
924        let registry = create_test_registry();
925        let key = Identifier::vanilla_static("stone");
926        let stone = registry.by_key(&key).expect("stone should exist");
927
928        let result = registry.state_id_from_properties(&key, &[("power", "1")]);
929        assert!(
930            result.is_none(),
931            "Should return None for invalid property on propertyless block"
932        );
933
934        let result = registry.state_id_from_block_defaulted_properties(stone, [("power", "1")]);
935        assert!(
936            result.is_none(),
937            "Should return None for invalid defaulted property on propertyless block"
938        );
939    }
940
941    #[test]
942    fn test_stone_no_properties() {
943        let registry = create_test_registry();
944        let key = Identifier::vanilla_static("stone");
945
946        // Stone has no properties
947        let stone = registry.by_key(&key).expect("stone should exist");
948        assert!(stone.properties.is_empty());
949
950        // Should still work with empty properties
951        let state_id = registry
952            .state_id_from_properties(&key, &[])
953            .expect("Should find state");
954
955        let retrieved = registry.get_properties(state_id);
956        assert!(retrieved.is_empty());
957    }
958
959    #[test]
960    fn test_all_redstone_power_levels() {
961        let registry = create_test_registry();
962        let key = Identifier::vanilla_static("redstone_wire");
963
964        // Test all 16 power levels
965        for power in 0..=15 {
966            let power_str = power.to_string();
967            let props = [("power", power_str.as_str())];
968
969            let state_id = registry
970                .state_id_from_properties(&key, &props)
971                .unwrap_or_else(|| panic!("Should find state for power {}", power));
972
973            let retrieved = registry.get_properties(state_id);
974            let found_power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
975            assert_eq!(
976                found_power.1,
977                power_str.as_str(),
978                "Power level {} mismatch",
979                power
980            );
981        }
982    }
983
984    #[test]
985    #[cfg(feature = "minecraft-src")]
986    fn test_all_block_state_ids_match_minecraft() {
987        use rustc_hash::FxHashMap as HashMap;
988        use std::fs;
989
990        #[derive(serde::Deserialize)]
991        struct BlockState {
992            id: u16,
993            #[serde(default)]
994            properties: HashMap<String, String>,
995            #[serde(default)]
996            default: bool,
997        }
998
999        #[derive(serde::Deserialize)]
1000        struct BlockData {
1001            states: Vec<BlockState>,
1002        }
1003
1004        // Try multiple paths to find blocks.json
1005        let possible_paths = [
1006            "minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1007            "../minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1008        ];
1009        let json_content = possible_paths
1010            .iter()
1011            .find_map(|path| fs::read_to_string(path).ok())
1012            .expect("Failed to read blocks.json - make sure minecraft-src is available");
1013        let blocks: HashMap<String, BlockData> =
1014            serde_json::from_str(&json_content).expect("Failed to parse blocks.json");
1015
1016        let registry = create_test_registry();
1017        let mut errors = Vec::new();
1018
1019        for (block_name, block_data) in &blocks {
1020            // Strip "minecraft:" prefix
1021            let key = Identifier::vanilla_static(
1022                block_name
1023                    .strip_prefix("minecraft:")
1024                    .unwrap_or(block_name)
1025                    .to_string()
1026                    .leak(),
1027            );
1028
1029            let Some(block) = registry.by_key(&key) else {
1030                errors.push(format!("Block {} not found in registry", block_name));
1031                continue;
1032            };
1033
1034            // Verify default state
1035            for state in &block_data.states {
1036                if state.default {
1037                    let our_default = registry.get_default_state_id(block);
1038                    if our_default.0 != state.id {
1039                        errors.push(format!(
1040                            "{}: default state mismatch - expected {}, got {}",
1041                            block_name, state.id, our_default.0
1042                        ));
1043                    }
1044                }
1045            }
1046
1047            // Verify all states
1048            for state in &block_data.states {
1049                let props: Vec<(&str, &str)> = state
1050                    .properties
1051                    .iter()
1052                    .map(|(k, v)| (k.as_str(), v.as_str()))
1053                    .collect();
1054
1055                let Some(our_state_id) = registry.state_id_from_properties(&key, &props) else {
1056                    errors.push(format!(
1057                        "{}: failed to get state for properties {:?}",
1058                        block_name, props
1059                    ));
1060                    continue;
1061                };
1062
1063                if our_state_id.0 != state.id {
1064                    errors.push(format!(
1065                        "{}: state mismatch for {:?} - expected {}, got {}",
1066                        block_name, props, state.id, our_state_id.0
1067                    ));
1068                }
1069            }
1070        }
1071
1072        if !errors.is_empty() {
1073            // Print first 20 errors for readability
1074            let display_errors: String = errors
1075                .iter()
1076                .take(20)
1077                .cloned()
1078                .collect::<Vec<_>>()
1079                .join("\n");
1080            panic!(
1081                "Found {} state ID mismatches:\n{}{}",
1082                errors.len(),
1083                display_errors,
1084                if errors.len() > 20 {
1085                    format!("\n... and {} more", errors.len() - 20)
1086                } else {
1087                    String::new()
1088                }
1089            );
1090        }
1091    }
1092}