Skip to main content

steel_registry/blocks/
mod.rs

1#![cfg_attr(
2    test,
3    expect(
4        clippy::unwrap_used,
5        reason = "block registry tests assert required vanilla properties are present"
6    )
7)]
8
9pub mod behavior;
10pub mod block_state_ext;
11pub mod properties;
12pub mod shapes;
13
14use std::sync::OnceLock;
15
16use glam::DVec3;
17use rustc_hash::FxHashMap;
18
19use crate::blocks::behavior::BlockConfig;
20use crate::blocks::properties::Property;
21use crate::blocks::shapes::ShapeChannel;
22use crate::fluid::{FluidRef, FluidState};
23use crate::{RegistryExt, RegistryTags, TaggedRegistryExt};
24use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey};
25
26/// Function type for shape lookups. Takes a state offset and returns the shape.
27pub type ShapeFn = fn(u16) -> shapes::VoxelShape;
28/// Function type for light-property lookups. Takes a state offset and returns extracted vanilla properties.
29pub type LightPropertiesFn = fn(u16) -> BlockLightProperties;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct BlockLightProperties {
33    pub light_emission: u8,
34    pub light_dampening: u8,
35    pub use_shape_for_light_occlusion: bool,
36}
37
38impl BlockLightProperties {
39    pub const OPAQUE_FULL_BLOCK: Self = Self {
40        light_emission: 0,
41        light_dampening: 15,
42        use_shape_for_light_occlusion: false,
43    };
44}
45
46#[derive(Debug, Clone, Copy)]
47pub struct StateBooleanOverwrite {
48    pub offset: u16,
49    pub value: bool,
50}
51
52impl StateBooleanOverwrite {
53    #[must_use]
54    pub const fn new(offset: u16, value: bool) -> Self {
55        Self { offset, value }
56    }
57}
58
59#[derive(Debug, Clone, Copy)]
60pub struct StateBooleanData {
61    pub default: bool,
62    pub overwrites: &'static [StateBooleanOverwrite],
63}
64
65impl StateBooleanData {
66    pub const TRUE: Self = Self::new(true, &[]);
67    pub const FALSE: Self = Self::new(false, &[]);
68
69    #[must_use]
70    pub const fn new(default: bool, overwrites: &'static [StateBooleanOverwrite]) -> Self {
71        Self {
72            default,
73            overwrites,
74        }
75    }
76
77    #[must_use]
78    pub fn value(self, offset: u16) -> bool {
79        self.overwrites
80            .iter()
81            .find(|overwrite| overwrite.offset == offset)
82            .map_or(self.default, |overwrite| overwrite.value)
83    }
84}
85
86#[derive(Debug, Clone, Copy)]
87pub struct StateFluidOverwrite {
88    pub offset: u16,
89    pub value: FluidState,
90}
91
92impl StateFluidOverwrite {
93    #[must_use]
94    pub const fn new(offset: u16, value: FluidState) -> Self {
95        Self { offset, value }
96    }
97}
98
99#[derive(Debug, Clone, Copy)]
100pub struct StateFluidData {
101    pub default: FluidState,
102    pub overwrites: &'static [StateFluidOverwrite],
103}
104
105impl StateFluidData {
106    pub const EMPTY: Self = Self::new(FluidState::EMPTY, &[]);
107
108    #[must_use]
109    pub const fn new(default: FluidState, overwrites: &'static [StateFluidOverwrite]) -> Self {
110        Self {
111            default,
112            overwrites,
113        }
114    }
115
116    #[must_use]
117    pub fn value(self, offset: u16) -> FluidState {
118        self.overwrites
119            .iter()
120            .find(|overwrite| overwrite.offset == offset)
121            .map_or(self.default, |overwrite| overwrite.value)
122    }
123}
124
125/// Immutable ticking metadata flattened by global block-state ID during registration.
126///
127/// The fields are packed instead of embedding `FluidState` plus separate booleans, while
128/// retaining direct `FluidRef` access on the random-tick hot path.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct BlockStateTickingMetadata {
131    fluid: FluidRef,
132    amount: u8,
133    flags: u8,
134}
135
136impl BlockStateTickingMetadata {
137    const FALLING: u8 = 1 << 0;
138    const RANDOMLY_TICKING_BLOCK: u8 = 1 << 1;
139    const RANDOMLY_TICKING_FLUID: u8 = 1 << 2;
140    const IS_AIR: u8 = 1 << 3;
141    const HAS_FLUID: u8 = 1 << 4;
142
143    #[must_use]
144    pub const fn new(fluid_state: FluidState, randomly_ticking_block: bool, is_air: bool) -> Self {
145        let mut flags = 0;
146        if fluid_state.falling {
147            flags |= Self::FALLING;
148        }
149        if randomly_ticking_block {
150            flags |= Self::RANDOMLY_TICKING_BLOCK;
151        }
152        if fluid_state.fluid_id.is_randomly_ticking {
153            flags |= Self::RANDOMLY_TICKING_FLUID;
154        }
155        if is_air {
156            flags |= Self::IS_AIR;
157        }
158        if !fluid_state.is_empty() {
159            flags |= Self::HAS_FLUID;
160        }
161        Self {
162            fluid: fluid_state.fluid_id,
163            amount: fluid_state.amount,
164            flags,
165        }
166    }
167
168    #[must_use]
169    pub const fn fluid_state(self) -> FluidState {
170        FluidState::new(self.fluid, self.amount, self.flags & Self::FALLING != 0)
171    }
172
173    #[must_use]
174    pub const fn randomly_ticking_block(self) -> bool {
175        self.flags & Self::RANDOMLY_TICKING_BLOCK != 0
176    }
177
178    #[must_use]
179    pub const fn randomly_ticking_fluid(self) -> bool {
180        self.flags & Self::RANDOMLY_TICKING_FLUID != 0
181    }
182
183    #[must_use]
184    pub const fn is_air(self) -> bool {
185        self.flags & Self::IS_AIR != 0
186    }
187
188    #[must_use]
189    pub const fn has_fluid(self) -> bool {
190        self.flags & Self::HAS_FLUID != 0
191    }
192}
193
194pub struct Block {
195    pub key: Identifier,
196    pub config: BlockConfig,
197    pub properties: &'static [&'static dyn Property],
198    pub default_state_offset: u16,
199    /// Vanilla `BlockState.isSuffocating` values indexed by block-local state offset.
200    pub suffocating: StateBooleanData,
201    /// Vanilla `BlockState.isRedstoneConductor` values indexed by block-local state offset.
202    pub redstone_conductor: StateBooleanData,
203    /// Vanilla `BlockState.getFluidState` values indexed by block-local state offset.
204    pub fluid_state: StateFluidData,
205    /// Vanilla `BlockState.isRandomlyTicking` values indexed by block-local state offset.
206    pub randomly_ticking: StateBooleanData,
207    /// Extracted vanilla light properties indexed by block-local state offset.
208    pub light_properties: LightPropertiesFn,
209    /// Function to get collision shape for a state offset
210    pub collision_shape: ShapeFn,
211    /// Function to get block support shape for a state offset
212    pub support_shape: ShapeFn,
213    /// Function to get outline shape for a state offset
214    pub outline_shape: ShapeFn,
215    /// Function to get occlusion shape for a state offset
216    pub occlusion_shape: ShapeFn,
217    /// Function to get interaction shape for a state offset
218    pub interaction_shape: ShapeFn,
219    /// Function to get visual shape for a state offset
220    pub visual_shape: ShapeFn,
221    /// Shape channels whose extracted boxes are normalized and need positional offset.
222    pub shape_offsets: shapes::ShapeOffsetFlags,
223    /// Cached registry ID, set during registration for O(1) lookup on hot paths.
224    pub id: OnceLock<usize>,
225}
226
227impl std::fmt::Debug for Block {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        f.debug_struct("Block")
230            .field("key", &self.key)
231            .field("config", &self.config)
232            .field("properties", &self.properties)
233            .field("default_state_offset", &self.default_state_offset)
234            .finish_non_exhaustive()
235    }
236}
237
238/// Default shape function that returns a full block.
239const fn full_block_shape(_offset: u16) -> shapes::VoxelShape {
240    shapes::VoxelShape::FULL_BLOCK
241}
242
243const fn opaque_full_block_light_properties(_offset: u16) -> BlockLightProperties {
244    BlockLightProperties::OPAQUE_FULL_BLOCK
245}
246
247/// Default interaction shape function that returns an empty shape.
248const fn empty_shape(_offset: u16) -> shapes::VoxelShape {
249    shapes::VoxelShape::EMPTY
250}
251
252impl Block {
253    pub const fn new(
254        key: Identifier,
255        config: BlockConfig,
256        properties: &'static [&'static dyn Property],
257    ) -> Self {
258        let randomly_ticking = StateBooleanData::new(config.is_randomly_ticking, &[]);
259        Self {
260            key,
261            config,
262            properties,
263            default_state_offset: 0,
264            suffocating: StateBooleanData::TRUE,
265            redstone_conductor: StateBooleanData::TRUE,
266            fluid_state: StateFluidData::EMPTY,
267            randomly_ticking,
268            light_properties: opaque_full_block_light_properties,
269            collision_shape: full_block_shape,
270            support_shape: full_block_shape,
271            outline_shape: full_block_shape,
272            occlusion_shape: full_block_shape,
273            interaction_shape: empty_shape,
274            visual_shape: full_block_shape,
275            shape_offsets: shapes::ShapeOffsetFlags::NONE,
276            id: OnceLock::new(),
277        }
278    }
279
280    /// Sets the shape functions for this block.
281    pub const fn with_shapes(
282        mut self,
283        collision: ShapeFn,
284        support: ShapeFn,
285        outline: ShapeFn,
286        occlusion: ShapeFn,
287        interaction: ShapeFn,
288        visual: ShapeFn,
289    ) -> Self {
290        self.collision_shape = collision;
291        self.support_shape = support;
292        self.outline_shape = outline;
293        self.occlusion_shape = occlusion;
294        self.interaction_shape = interaction;
295        self.visual_shape = visual;
296        self
297    }
298
299    /// Sets the extracted vanilla `BlockState.isSuffocating` values for this block.
300    pub const fn with_suffocating(mut self, suffocating: StateBooleanData) -> Self {
301        self.suffocating = suffocating;
302        self
303    }
304
305    /// Sets the extracted per-state redstone-conductor predicate.
306    pub const fn with_redstone_conductor(mut self, redstone_conductor: StateBooleanData) -> Self {
307        self.redstone_conductor = redstone_conductor;
308        self
309    }
310
311    /// Sets the extracted per-state fluid values.
312    pub const fn with_fluid_state(mut self, fluid_state: StateFluidData) -> Self {
313        self.fluid_state = fluid_state;
314        self
315    }
316
317    /// Sets the extracted per-state random-tick predicate.
318    pub const fn with_randomly_ticking(mut self, randomly_ticking: StateBooleanData) -> Self {
319        self.randomly_ticking = randomly_ticking;
320        self
321    }
322
323    /// Sets the extracted vanilla light properties for this block.
324    pub const fn with_light_properties(mut self, light_properties: LightPropertiesFn) -> Self {
325        self.light_properties = light_properties;
326        self
327    }
328
329    /// Sets which shape channels use the block state's positional offset.
330    pub const fn with_shape_offsets(mut self, offsets: shapes::ShapeOffsetFlags) -> Self {
331        self.shape_offsets = offsets;
332        self
333    }
334
335    /// Gets the collision shape for a given state offset.
336    #[inline]
337    pub fn get_collision_shape(&self, offset: u16) -> shapes::VoxelShape {
338        (self.collision_shape)(offset)
339    }
340
341    /// Gets the block support shape for a given state offset.
342    #[inline]
343    pub fn get_support_shape(&self, offset: u16) -> shapes::VoxelShape {
344        (self.support_shape)(offset)
345    }
346
347    /// Gets the outline shape for a given state offset.
348    #[inline]
349    pub fn get_outline_shape(&self, offset: u16) -> shapes::VoxelShape {
350        (self.outline_shape)(offset)
351    }
352
353    /// Gets the occlusion shape for a given state offset.
354    #[inline]
355    pub fn get_occlusion_shape(&self, offset: u16) -> shapes::VoxelShape {
356        (self.occlusion_shape)(offset)
357    }
358
359    /// Gets the interaction shape for a given state offset.
360    #[inline]
361    pub fn get_interaction_shape(&self, offset: u16) -> shapes::VoxelShape {
362        (self.interaction_shape)(offset)
363    }
364
365    /// Gets the visual shape for a given state offset.
366    #[inline]
367    pub fn get_visual_shape(&self, offset: u16) -> shapes::VoxelShape {
368        (self.visual_shape)(offset)
369    }
370
371    #[inline]
372    pub fn get_light_properties(&self, offset: u16) -> BlockLightProperties {
373        (self.light_properties)(offset)
374    }
375
376    #[must_use]
377    pub fn get_ticking_metadata(&self, offset: u16) -> BlockStateTickingMetadata {
378        BlockStateTickingMetadata::new(
379            self.fluid_state.value(offset),
380            self.randomly_ticking.value(offset),
381            self.config.is_air,
382        )
383    }
384
385    /// Returns the vanilla block-state positional offset for this block.
386    #[must_use]
387    pub fn offset_at(&self, pos: BlockPos) -> DVec3 {
388        self.config.offset_at(pos)
389    }
390
391    /// Sets the default state offset for this block.
392    /// The offset is relative to the block's base state ID.
393    ///
394    /// For easier usage, consider using `with_default_state_from_indices` or the
395    /// `default_state!` macro instead of calculating the offset manually.
396    ///
397    /// # Example
398    /// ```ignore
399    /// const REPEATER: Block = Block::new("repeater", props, &[...])
400    ///     .with_default_state(4);
401    /// ```
402    pub(crate) const fn with_default_state(mut self, offset: u16) -> Self {
403        self.default_state_offset = offset;
404
405        self
406    }
407
408    /// Const helper to calculate state offset from property indices and counts.
409    /// Properties are processed in reverse order to match Minecraft's encoding
410    /// (last property = inner loop with multiplier 1).
411    #[must_use]
412    pub const fn calculate_offset(property_indices: &[usize], property_counts: &[usize]) -> u16 {
413        let mut offset = 0u16;
414        let mut multiplier = 1u16;
415        let len = property_indices.len();
416
417        // Iterate in reverse order: last property first (inner loop)
418        let mut i = len;
419        while i > 0 {
420            i -= 1;
421            offset += property_indices[i] as u16 * multiplier;
422            multiplier *= property_counts[i] as u16;
423        }
424
425        offset
426    }
427
428    #[must_use]
429    pub fn default_state(&'static self) -> BlockStateId {
430        crate::REGISTRY.blocks.get_default_state_id(self)
431    }
432
433    /// Total number of distinct states (the product of every property's value count).
434    #[must_use]
435    pub fn state_count(&self) -> u16 {
436        self.properties
437            .iter()
438            .map(|property| property.value_count() as u16)
439            .product()
440    }
441
442    /// Returns `true` if this block is tagged with the given tag.
443    pub fn has_tag(&'static self, tag: &Identifier) -> bool {
444        crate::REGISTRY.blocks.is_in_tag(self, tag)
445    }
446}
447
448pub type BlockRef = &'static Block;
449
450// The central registry for all blocks.
451pub struct BlockRegistry {
452    blocks_by_id: Vec<BlockRef>,
453    blocks_by_key: FxHashMap<Identifier, usize>,
454    tags: RegistryTags,
455    allows_registering: bool,
456    pub state_to_block_lookup: Vec<BlockRef>,
457    /// Maps state IDs to block IDs (parallel to `state_to_block_lookup` for O(1) lookup)
458    pub state_to_block_id: Vec<usize>,
459    /// Behavior-independent metadata indexed directly by global block-state ID.
460    state_ticking_metadata: Vec<BlockStateTickingMetadata>,
461    /// Maps block IDs to their base state ID
462    pub block_to_base_state: Vec<u16>,
463    /// The next state ID to be allocated
464    pub next_state_id: u16,
465}
466
467// SAFETY: This Steel-owned key uniquely identifies the block registry.
468unsafe impl DowncastType for BlockRegistry {
469    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:registry/block");
470}
471
472impl Default for BlockRegistry {
473    fn default() -> Self {
474        Self::new()
475    }
476}
477
478impl BlockRegistry {
479    // Creates a new, empty registry.
480    #[must_use]
481    pub fn new() -> Self {
482        Self {
483            blocks_by_id: Vec::new(),
484            blocks_by_key: FxHashMap::default(),
485            tags: RegistryTags::default(),
486            allows_registering: true,
487            state_to_block_lookup: Vec::new(),
488            state_to_block_id: Vec::new(),
489            state_ticking_metadata: Vec::new(),
490            block_to_base_state: Vec::new(),
491            next_state_id: 0,
492        }
493    }
494
495    pub fn register(&mut self, block: BlockRef) -> usize {
496        assert!(
497            self.allows_registering,
498            "Cannot register blocks after the registry has been frozen"
499        );
500
501        let id = self.blocks_by_id.len();
502        let base_state_id = self.next_state_id;
503
504        let cached = block.id.get_or_init(|| id);
505        assert_eq!(*cached, id, "block registered with conflicting id");
506        self.blocks_by_key.insert(block.key.clone(), id);
507        self.blocks_by_id.push(block);
508        self.block_to_base_state.push(base_state_id);
509
510        let state_count = block.state_count();
511        for offset in 0..state_count {
512            self.state_to_block_lookup.push(block);
513            self.state_to_block_id.push(id);
514            self.state_ticking_metadata
515                .push(block.get_ticking_metadata(offset));
516        }
517
518        self.next_state_id += state_count;
519
520        id
521    }
522
523    fn try_block_index(&self, block: BlockRef) -> Option<usize> {
524        if let Some(id) = block.id.get().copied()
525            && self
526                .blocks_by_id
527                .get(id)
528                .is_some_and(|registered| *registered == block)
529        {
530            return Some(id);
531        }
532
533        self.blocks_by_key.get(&block.key).copied()
534    }
535
536    fn block_index(&self, block: BlockRef) -> usize {
537        let Some(id) = self.try_block_index(block) else {
538            panic!("Block not found");
539        };
540        id
541    }
542
543    #[must_use]
544    pub fn get_base_state_id(&self, block: BlockRef) -> BlockStateId {
545        let id = self.block_index(block);
546        BlockStateId(self.block_to_base_state[id])
547    }
548
549    /// Gets the default state ID for a block (base state + default offset)
550    #[must_use]
551    pub fn get_default_state_id(&self, block: BlockRef) -> BlockStateId {
552        let id = self.block_index(block);
553        let base = self.block_to_base_state[id];
554        BlockStateId(base + block.default_state_offset)
555    }
556
557    #[must_use]
558    pub fn by_state_id(&self, state_id: BlockStateId) -> Option<BlockRef> {
559        self.state_to_block_lookup.get(state_id.0 as usize).copied()
560    }
561
562    #[must_use]
563    pub fn get_ticking_metadata(
564        &self,
565        state_id: BlockStateId,
566    ) -> Option<BlockStateTickingMetadata> {
567        self.state_ticking_metadata
568            .get(state_id.0 as usize)
569            .copied()
570    }
571
572    #[must_use]
573    pub fn get_properties(&self, id: BlockStateId) -> Vec<(&'static str, &'static str)> {
574        let block = self.by_state_id(id).expect("Invalid state ID");
575
576        // If block has no properties, return empty vec
577        if block.properties.is_empty() {
578            return Vec::new();
579        }
580
581        // Get the base state ID for this block (O(1) lookup)
582        let block_id = self.state_to_block_id[id.0 as usize];
583        let base_state_id = self.block_to_base_state[block_id];
584
585        // Calculate the relative state index
586        let relative_index = id.0 - base_state_id;
587
588        Self::decode_property_indices(block, relative_index)
589            .into_iter()
590            .zip(block.properties)
591            .map(|(value_index, prop)| (prop.get_name(), prop.value_name_from_index(value_index)))
592            .collect()
593    }
594
595    /// Gets the state ID for a block with the given properties.
596    ///
597    /// Returns `None` if the block key is unknown or if any property name/value is invalid.
598    ///
599    /// Properties can be provided in any order. Missing properties will use the block's
600    /// default values (typically index 0 for each property).
601    #[must_use]
602    pub fn state_id_from_properties(
603        &self,
604        key: &Identifier,
605        properties: &[(&str, &str)],
606    ) -> Option<BlockStateId> {
607        let block = self.by_key(key)?;
608        self.state_id_from_block_properties(block, properties)
609    }
610
611    /// Gets the state ID for a block with the given properties.
612    ///
613    /// Returns `None` if the block is not registered or if any property
614    /// name/value is invalid.
615    #[must_use]
616    pub fn state_id_from_block_properties(
617        &self,
618        block: BlockRef,
619        properties: &[(&str, &str)],
620    ) -> Option<BlockStateId> {
621        let block_id = self.try_block_index(block)?;
622        let base_state_id = self.block_to_base_state[block_id];
623
624        let mut property_indices = vec![0usize; block.properties.len()];
625        Self::apply_property_overrides(block, &mut property_indices, properties.iter().copied())?;
626
627        Some(BlockStateId(
628            base_state_id + Self::encode_property_indices(block, &property_indices),
629        ))
630    }
631
632    /// Gets the state ID for a block by applying properties over that block's
633    /// registered default state.
634    ///
635    /// Returns `None` if the block is not registered or if any property
636    /// name/value is invalid.
637    #[must_use]
638    pub fn state_id_from_block_defaulted_properties<'a>(
639        &self,
640        block: BlockRef,
641        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
642    ) -> Option<BlockStateId> {
643        let block_id = self.try_block_index(block)?;
644        let base_state_id = self.block_to_base_state[block_id];
645
646        let mut property_indices = Self::decode_property_indices(block, block.default_state_offset);
647        Self::apply_property_overrides(block, &mut property_indices, properties)?;
648
649        Some(BlockStateId(
650            base_state_id + Self::encode_property_indices(block, &property_indices),
651        ))
652    }
653
654    /// Applies one dynamically named property value to an existing state.
655    ///
656    /// Returns `None` when the state is invalid or the state's block does not
657    /// define the named property/value pair. This mirrors Vanilla's
658    /// `StateHolder.trySetValue` path used by parsed command block inputs.
659    #[must_use]
660    pub fn try_set_property_by_name(
661        &self,
662        id: BlockStateId,
663        name: &str,
664        value: &str,
665    ) -> Option<BlockStateId> {
666        let block = self.by_state_id(id)?;
667        let block_id = *self.state_to_block_id.get(id.0 as usize)?;
668        let base_state_id = *self.block_to_base_state.get(block_id)?;
669        let property_index = block
670            .properties
671            .iter()
672            .position(|property| property.get_name() == name)?;
673        let property = block.properties[property_index];
674        let new_value_index = (0..property.value_count())
675            .find(|&index| property.value_name_from_index(index) == value)?;
676        let relative_index = id.0.checked_sub(base_state_id)?;
677        let stride = Self::property_stride(block, property_index);
678        let old_value_index = usize::from(relative_index / stride % property.value_count() as u16);
679        let new_relative_index = if new_value_index >= old_value_index {
680            relative_index.checked_add((new_value_index - old_value_index) as u16 * stride)?
681        } else {
682            relative_index.checked_sub((old_value_index - new_value_index) as u16 * stride)?
683        };
684
685        Some(BlockStateId(base_state_id + new_relative_index))
686    }
687
688    /// Returns every state id of `block` whose properties match all `(name, value)` pairs
689    /// in `filter`. An empty filter yields all states of the block (vanilla's
690    /// `getStatesOfBlock`); a non-empty filter keeps only matching states (vanilla's
691    /// `BED_HEADS`-style predicate).
692    #[must_use]
693    pub fn matching_states(&self, block: BlockRef, filter: &[(&str, &str)]) -> Vec<BlockStateId> {
694        let block_id = self.block_index(block);
695        let base_state_id = self.block_to_base_state[block_id];
696
697        (0..block.state_count())
698            .map(|offset| BlockStateId(base_state_id + offset))
699            .filter(|&state_id| {
700                let properties = self.get_properties(state_id);
701                filter
702                    .iter()
703                    .all(|(name, value)| properties.iter().any(|(n, v)| n == name && v == value))
704            })
705            .collect()
706    }
707
708    fn decode_property_indices(block: BlockRef, mut offset: u16) -> Vec<usize> {
709        let mut property_indices = vec![0; block.properties.len()];
710
711        for (i, prop) in block.properties.iter().enumerate().rev() {
712            let count = prop.value_count() as u16;
713            property_indices[i] = (offset % count) as usize;
714            offset /= count;
715        }
716
717        property_indices
718    }
719
720    fn apply_property_overrides<'a>(
721        block: BlockRef,
722        property_indices: &mut [usize],
723        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
724    ) -> Option<()> {
725        for (prop_name, prop_value) in properties {
726            let prop_idx = block
727                .properties
728                .iter()
729                .position(|p| p.get_name() == prop_name)?;
730
731            let prop = block.properties[prop_idx];
732            let value_idx = (0..prop.value_count())
733                .find(|&index| prop.value_name_from_index(index) == prop_value)?;
734
735            property_indices[prop_idx] = value_idx;
736        }
737
738        Some(())
739    }
740
741    fn encode_property_indices(block: BlockRef, property_indices: &[usize]) -> u16 {
742        let mut offset = 0u16;
743        let mut multiplier = 1u16;
744        for (idx, prop) in property_indices.iter().zip(block.properties.iter()).rev() {
745            offset += *idx as u16 * multiplier;
746            multiplier *= prop.value_count() as u16;
747        }
748
749        offset
750    }
751
752    fn property_stride(block: BlockRef, property_index: usize) -> u16 {
753        block.properties[property_index + 1..]
754            .iter()
755            .map(|property| property.value_count() as u16)
756            .product()
757    }
758
759    // Panics if that property isn't supposed to be on this block.
760    pub fn get_property<P: Property>(&self, id: BlockStateId, property: &P) -> P::Value {
761        self.try_get_property(id, property)
762            .expect("Property not found on this block")
763    }
764
765    /// Gets the value of a property, returning `None` if the block doesn't have this property.
766    #[must_use]
767    pub fn try_get_property<P: Property>(
768        &self,
769        id: BlockStateId,
770        property: &P,
771    ) -> Option<P::Value> {
772        let block = self.by_state_id(id).expect("Invalid state ID");
773
774        // Find the property index in the block's property list
775        let property_index = block
776            .properties
777            .iter()
778            .position(|prop| prop.get_name() == property.get_name())?;
779
780        // Get the base state ID for this block (O(1) lookup)
781        let block_id = self.state_to_block_id[id.0 as usize];
782        let base_state_id = self.block_to_base_state[block_id];
783
784        // Calculate the relative state index
785        let relative_index = id.0 - base_state_id;
786
787        let block_property = block.properties[property_index];
788        let stride = Self::property_stride(block, property_index);
789        let value_index =
790            usize::from(relative_index / stride % block_property.value_count() as u16);
791        let block_value = block_property.value_name_from_index(value_index);
792
793        property.get_value(block_value)
794    }
795
796    // Panics if that property isn't supposed to be on this block.
797    pub fn set_property<P: Property>(
798        &self,
799        id: BlockStateId,
800        property: &P,
801        value: P::Value,
802    ) -> BlockStateId {
803        let block = self.by_state_id(id).expect("Invalid state ID");
804
805        // Find the property index in the block's property list
806        let property_index = block
807            .properties
808            .iter()
809            .position(|prop| prop.get_name() == property.get_name())
810            .unwrap_or_else(|| {
811                panic!(
812                    "Property {} not found on block {}",
813                    property.get_name(),
814                    block.key
815                )
816            });
817
818        // Get the base state ID for this block (O(1) lookup)
819        let block_id = self.state_to_block_id[id.0 as usize];
820        let base_state_id = self.block_to_base_state[block_id];
821
822        // Calculate the relative state index
823        let relative_index = id.0 - base_state_id;
824
825        let caller_value_index = property.get_internal_index(&value);
826        let value_name = property.value_name_from_index(caller_value_index);
827        let block_property = block.properties[property_index];
828        let Some(new_value_index) = (0..block_property.value_count())
829            .find(|&index| block_property.value_name_from_index(index) == value_name)
830        else {
831            panic!(
832                "Value {} for property {} not found on block {}",
833                value_name,
834                property.get_name(),
835                block.key
836            );
837        };
838        let stride = Self::property_stride(block, property_index);
839        let old_value_index =
840            usize::from(relative_index / stride % block_property.value_count() as u16);
841        let new_relative_index = if new_value_index >= old_value_index {
842            relative_index + (new_value_index - old_value_index) as u16 * stride
843        } else {
844            relative_index - (old_value_index - new_value_index) as u16 * stride
845        };
846
847        BlockStateId(base_state_id + new_relative_index)
848    }
849
850    pub fn iter(&self) -> impl Iterator<Item = (usize, BlockRef)> + '_ {
851        self.blocks_by_id
852            .iter()
853            .enumerate()
854            .map(|(id, &block)| (id, block))
855    }
856}
857
858crate::impl_registry_ext!(BlockRegistry, Block, blocks_by_id, blocks_by_key);
859
860crate::impl_registry_entry_eq!(Block);
861
862impl crate::RegistryEntry for Block {
863    fn key(&self) -> &Identifier {
864        &self.key
865    }
866
867    fn try_id(&self) -> Option<usize> {
868        self.id.get().copied()
869    }
870}
871crate::impl_tagged_registry!(BlockRegistry, blocks_by_key, "block");
872
873// Shape lookup methods
874impl BlockRegistry {
875    fn block_and_state_offset(&self, state_id: BlockStateId) -> Option<(BlockRef, u16)> {
876        let block = self
877            .state_to_block_lookup
878            .get(state_id.0 as usize)
879            .copied()?;
880        let block_id = self
881            .state_to_block_id
882            .get(state_id.0 as usize)
883            .copied()
884            .unwrap_or(0);
885        let base_state = self.block_to_base_state.get(block_id).copied().unwrap_or(0);
886        let offset = state_id.0.saturating_sub(base_state);
887        Some((block, offset))
888    }
889
890    fn static_shape_for_state(
891        &self,
892        state_id: BlockStateId,
893        shape: fn(&Block, u16) -> shapes::VoxelShape,
894    ) -> shapes::VoxelShape {
895        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
896            return shapes::VoxelShape::FULL_BLOCK;
897        };
898        shape(block, offset)
899    }
900
901    fn offset_shape_for_state(
902        &self,
903        state_id: BlockStateId,
904        pos: BlockPos,
905        channel: ShapeChannel,
906        shape: fn(&Block, u16) -> shapes::VoxelShape,
907    ) -> shapes::OffsetVoxelShape {
908        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
909            return shapes::OffsetVoxelShape::without_offset(shapes::VoxelShape::FULL_BLOCK);
910        };
911
912        let shape = shape(block, offset);
913        let offset = if block.shape_offsets.uses_offset(channel) {
914            block.offset_at(pos)
915        } else {
916            DVec3::ZERO
917        };
918        shapes::OffsetVoxelShape::new(shape, offset)
919    }
920
921    /// Gets the collision shape for a block state.
922    ///
923    /// For simple blocks this is typically a single full-block box.
924    /// For complex blocks like fences, this may be multiple boxes.
925    #[must_use]
926    pub fn get_static_collision_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
927        self.static_shape_for_state(state_id, Block::get_collision_shape)
928    }
929
930    /// Returns vanilla `BlockState.isSuffocating`.
931    #[must_use]
932    pub fn is_suffocating(&self, state_id: BlockStateId) -> bool {
933        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
934            return false;
935        };
936        block.suffocating.value(offset)
937    }
938
939    /// Returns the extracted static `BlockState.isRedstoneConductor` value.
940    ///
941    /// Dynamic block behaviors may override this value using the live level and position.
942    #[must_use]
943    pub fn is_static_redstone_conductor(&self, state_id: BlockStateId) -> bool {
944        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
945            return false;
946        };
947        block.redstone_conductor.value(offset)
948    }
949
950    #[must_use]
951    pub fn get_collision_shape_at(
952        &self,
953        state_id: BlockStateId,
954        pos: BlockPos,
955    ) -> shapes::OffsetVoxelShape {
956        self.offset_shape_for_state(
957            state_id,
958            pos,
959            ShapeChannel::Collision,
960            Block::get_collision_shape,
961        )
962    }
963
964    /// Gets the block support shape for a block state.
965    ///
966    /// Vanilla support checks use `BlockState.getBlockSupportShape`, not collision shape,
967    /// for `isFaceSturdy` and multiface side attachment.
968    #[must_use]
969    pub fn get_static_support_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
970        self.static_shape_for_state(state_id, Block::get_support_shape)
971    }
972
973    #[must_use]
974    pub fn get_support_shape_at(
975        &self,
976        state_id: BlockStateId,
977        pos: BlockPos,
978    ) -> shapes::OffsetVoxelShape {
979        self.offset_shape_for_state(
980            state_id,
981            pos,
982            ShapeChannel::Support,
983            Block::get_support_shape,
984        )
985    }
986
987    /// Gets the outline shape for a block state.
988    ///
989    /// This is the shape shown when the player targets the block.
990    /// Often the same as collision shape, but can differ (e.g., fences).
991    #[must_use]
992    pub fn get_static_outline_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
993        self.static_shape_for_state(state_id, Block::get_outline_shape)
994    }
995
996    #[must_use]
997    pub fn get_outline_shape_at(
998        &self,
999        state_id: BlockStateId,
1000        pos: BlockPos,
1001    ) -> shapes::OffsetVoxelShape {
1002        self.offset_shape_for_state(
1003            state_id,
1004            pos,
1005            ShapeChannel::Outline,
1006            Block::get_outline_shape,
1007        )
1008    }
1009
1010    /// Gets the occlusion shape for a block state.
1011    ///
1012    /// Vanilla caches this as `BlockState.getOcclusionShape()` and uses it for
1013    /// `isSolidRender`, light occlusion, and face occlusion.
1014    #[must_use]
1015    pub fn get_occlusion_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1016        self.static_shape_for_state(state_id, Block::get_occlusion_shape)
1017    }
1018
1019    /// Gets the interaction shape for a block state.
1020    ///
1021    /// Vanilla uses this as an interaction hit override after the primary raycast
1022    /// shape has already hit.
1023    #[must_use]
1024    pub fn get_static_interaction_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1025        self.static_shape_for_state(state_id, Block::get_interaction_shape)
1026    }
1027
1028    #[must_use]
1029    pub fn get_interaction_shape_at(
1030        &self,
1031        state_id: BlockStateId,
1032        pos: BlockPos,
1033    ) -> shapes::OffsetVoxelShape {
1034        self.offset_shape_for_state(
1035            state_id,
1036            pos,
1037            ShapeChannel::Interaction,
1038            Block::get_interaction_shape,
1039        )
1040    }
1041
1042    /// Gets the visual shape for a block state.
1043    ///
1044    /// Vanilla uses this for visual raycasts; it defaults to collision shape but
1045    /// differs for a few blocks such as fences, mud, soul sand, and powder snow.
1046    #[must_use]
1047    pub fn get_static_visual_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1048        self.static_shape_for_state(state_id, Block::get_visual_shape)
1049    }
1050
1051    #[must_use]
1052    pub fn get_visual_shape_at(
1053        &self,
1054        state_id: BlockStateId,
1055        pos: BlockPos,
1056    ) -> shapes::OffsetVoxelShape {
1057        self.offset_shape_for_state(state_id, pos, ShapeChannel::Visual, Block::get_visual_shape)
1058    }
1059
1060    /// Gets all static shape channels for a block state.
1061    #[must_use]
1062    pub fn get_static_shapes(&self, state_id: BlockStateId) -> shapes::BlockShapes {
1063        shapes::BlockShapes::new(
1064            self.get_static_collision_shape(state_id),
1065            self.get_static_support_shape(state_id),
1066            self.get_static_outline_shape(state_id),
1067            self.get_occlusion_shape(state_id),
1068            self.get_static_interaction_shape(state_id),
1069            self.get_static_visual_shape(state_id),
1070        )
1071    }
1072
1073    #[must_use]
1074    pub fn get_light_properties(&self, state_id: BlockStateId) -> BlockLightProperties {
1075        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
1076            return BlockLightProperties::OPAQUE_FULL_BLOCK;
1077        };
1078        block.get_light_properties(offset)
1079    }
1080
1081    pub fn copy_matching_properties(&self, source: BlockStateId, target: BlockRef) -> BlockStateId {
1082        let props = self.get_properties(source);
1083        let matching: Vec<(&str, &str)> = props
1084            .iter()
1085            .filter(|(name, _)| target.properties.iter().any(|p| p.get_name() == *name))
1086            .copied()
1087            .collect();
1088        self.state_id_from_block_defaulted_properties(target, matching)
1089            .unwrap_or_else(|| self.get_default_state_id(target))
1090    }
1091}
1092
1093/// Macro to generate offset calculation from property values in all positions.
1094///
1095/// Takes property objects and their values, automatically converts to indices.
1096/// All properties must be specified in order.
1097///
1098/// # Note
1099/// For boolean properties, use `.index_of(value)` to handle the inverted encoding
1100/// (true=0, false=1 for Java compatibility).
1101///
1102/// # Example
1103/// ```ignore
1104/// use steel_registry::{offset, properties::{BlockStateProperties as Props, RedstoneSide}};
1105///
1106/// const WIRE: Block = Block::new("wire", behavior, PROPS)
1107///     .with_default_state(offset!(
1108///         Props::EAST_REDSTONE => RedstoneSide::Up,
1109///         Props::NORTH_REDSTONE => RedstoneSide::None,
1110///         Props::POWER => 10,
1111///         Props::ATTACHED => Props::ATTACHED.index_of(false)  // Bools need .index_of()
1112///     ));
1113/// ```
1114#[macro_export]
1115macro_rules! offset {
1116    ($($prop:expr => $value:expr),* $(,)?) => {{
1117        const INDICES: &[usize] = &[$($value as usize),*];
1118        const COUNTS: &[usize] = &[$($prop.value_count()),*];
1119        $crate::blocks::Block::calculate_offset(INDICES, COUNTS)
1120    }};
1121}
1122
1123/// Re-export for easier access
1124pub use offset;
1125use steel_utils::Identifier;
1126
1127#[cfg(test)]
1128mod tests {
1129    use super::*;
1130    use crate::blocks::properties::{BlockStateProperties, Direction};
1131    use crate::vanilla_blocks;
1132
1133    fn create_test_registry() -> BlockRegistry {
1134        let mut registry = BlockRegistry::new();
1135        vanilla_blocks::register_blocks(&mut registry);
1136        registry.freeze();
1137        registry
1138    }
1139
1140    #[test]
1141    fn tag_modification_keeps_order_and_membership_in_sync() {
1142        let mut registry = BlockRegistry::new();
1143        vanilla_blocks::register_blocks(&mut registry);
1144        let tag = Identifier::new_static("test", "ordered_membership");
1145        registry.register_tag(
1146            Identifier::new_static("test", "ordered_membership"),
1147            &["stone", "dirt"],
1148        );
1149
1150        assert!(registry.is_in_tag(&vanilla_blocks::STONE, &tag));
1151        assert_eq!(
1152            registry
1153                .iter_tag(&tag)
1154                .map(|block| block.key.path.as_ref())
1155                .collect::<Vec<_>>(),
1156            ["stone", "dirt"]
1157        );
1158
1159        registry.modify_tag(&tag, |_| {
1160            vec![
1161                Identifier::vanilla_static("oak_log"),
1162                Identifier::vanilla_static("dirt"),
1163            ]
1164        });
1165        registry.freeze();
1166
1167        assert!(!registry.is_in_tag(&vanilla_blocks::STONE, &tag));
1168        assert!(registry.is_in_tag(&vanilla_blocks::OAK_LOG, &tag));
1169        assert!(registry.is_in_tag(&vanilla_blocks::DIRT, &tag));
1170        assert_eq!(
1171            registry
1172                .iter_tag(&tag)
1173                .map(|block| block.key.path.as_ref())
1174                .collect::<Vec<_>>(),
1175            ["oak_log", "dirt"]
1176        );
1177    }
1178
1179    #[test]
1180    fn test_redstone_wire_properties() {
1181        let registry = create_test_registry();
1182        let redstone_wire = registry
1183            .by_key(&Identifier::vanilla_static("redstone_wire"))
1184            .expect("redstone_wire should exist");
1185
1186        // Redstone wire has 5 properties
1187        assert_eq!(redstone_wire.properties.len(), 5);
1188
1189        // Check property names
1190        let prop_names: Vec<&str> = redstone_wire
1191            .properties
1192            .iter()
1193            .map(|p| p.get_name())
1194            .collect();
1195        assert!(prop_names.contains(&"east"));
1196        assert!(prop_names.contains(&"north"));
1197        assert!(prop_names.contains(&"south"));
1198        assert!(prop_names.contains(&"west"));
1199        assert!(prop_names.contains(&"power"));
1200    }
1201
1202    #[test]
1203    fn test_redstone_wire_state_count() {
1204        let registry = create_test_registry();
1205
1206        // Redstone wire: 3 sides × 3 sides × 3 sides × 3 sides × 16 power levels = 1296 states
1207        // Actually checking the state count
1208        let redstone_wire = registry
1209            .by_key(&Identifier::vanilla_static("redstone_wire"))
1210            .expect("redstone_wire should exist");
1211
1212        let mut state_count = 1;
1213        for prop in redstone_wire.properties {
1214            state_count *= prop.get_possible_value_names().len();
1215        }
1216        assert_eq!(state_count, 3 * 3 * 3 * 3 * 16); // 1296
1217    }
1218
1219    #[test]
1220    fn test_get_properties_default_state() {
1221        let registry = create_test_registry();
1222        let redstone_wire = registry
1223            .by_key(&Identifier::vanilla_static("redstone_wire"))
1224            .expect("redstone_wire should exist");
1225
1226        let default_state = registry.get_default_state_id(redstone_wire);
1227        let properties = registry.get_properties(default_state);
1228
1229        // Default state should have all sides "none" and power 0
1230        assert_eq!(properties.len(), 5);
1231
1232        for (name, value) in &properties {
1233            match *name {
1234                "east" | "north" | "south" | "west" => {
1235                    assert_eq!(*value, "none", "Default side should be 'none'");
1236                }
1237                "power" => {
1238                    assert_eq!(*value, "0", "Default power should be '0'");
1239                }
1240                _ => panic!("Unexpected property: {name}"),
1241            }
1242        }
1243    }
1244
1245    #[test]
1246    fn test_state_id_from_properties_roundtrip() {
1247        let registry = create_test_registry();
1248        let key = Identifier::vanilla_static("redstone_wire");
1249
1250        // Test with specific properties
1251        let properties = [
1252            ("east", "up"),
1253            ("north", "side"),
1254            ("south", "none"),
1255            ("west", "up"),
1256            ("power", "15"),
1257        ];
1258
1259        let state_id = registry
1260            .state_id_from_properties(&key, &properties)
1261            .expect("Should find state");
1262
1263        // Get properties back and verify
1264        let retrieved = registry.get_properties(state_id);
1265        assert_eq!(retrieved.len(), 5);
1266
1267        for (name, value) in &properties {
1268            let found = retrieved
1269                .iter()
1270                .find(|(n, _)| n == name)
1271                .expect("Property should exist");
1272            assert_eq!(found.1, *value, "Property {name} mismatch");
1273        }
1274    }
1275
1276    #[test]
1277    fn test_state_id_from_properties_partial() {
1278        let registry = create_test_registry();
1279        let key = Identifier::vanilla_static("redstone_wire");
1280
1281        // Only specify some properties - others should default to index 0
1282        let partial_props = [("power", "10"), ("east", "side")];
1283
1284        let state_id = registry
1285            .state_id_from_properties(&key, &partial_props)
1286            .expect("Should find state");
1287
1288        let retrieved = registry.get_properties(state_id);
1289
1290        // Verify specified properties
1291        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1292        assert_eq!(power.1, "10");
1293
1294        let east = retrieved.iter().find(|(n, _)| *n == "east").unwrap();
1295        assert_eq!(east.1, "side");
1296
1297        // Unspecified properties should be at index 0 (first value in enum)
1298        let north = retrieved.iter().find(|(n, _)| *n == "north").unwrap();
1299        assert_eq!(north.1, "up"); // Index 0 is "up" for RedstoneSide
1300    }
1301
1302    #[test]
1303    fn dynamically_named_property_update_preserves_other_state_values() {
1304        let registry = create_test_registry();
1305        let wire = registry
1306            .state_id_from_block_defaulted_properties(
1307                &vanilla_blocks::REDSTONE_WIRE,
1308                [("east", "side"), ("power", "7")],
1309            )
1310            .expect("redstone wire state should exist");
1311
1312        let updated = registry
1313            .try_set_property_by_name(wire, "east", "up")
1314            .expect("dynamic property should update");
1315
1316        let properties = registry.get_properties(updated);
1317        assert!(properties.contains(&("east", "up")));
1318        assert!(properties.contains(&("power", "7")));
1319        assert!(
1320            registry
1321                .try_set_property_by_name(updated, "missing", "value")
1322                .is_none()
1323        );
1324    }
1325
1326    #[test]
1327    fn test_state_id_from_block_defaulted_properties_keeps_missing_defaults() {
1328        let registry = create_test_registry();
1329        let key = Identifier::vanilla_static("redstone_wire");
1330        let block = registry.by_key(&key).expect("redstone_wire should exist");
1331
1332        let state_id = registry
1333            .state_id_from_block_defaulted_properties(block, [("power", "10")])
1334            .expect("Should find state");
1335
1336        let retrieved = registry.get_properties(state_id);
1337
1338        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1339        assert_eq!(power.1, "10");
1340
1341        for direction in ["east", "north", "south", "west"] {
1342            let side = retrieved.iter().find(|(n, _)| *n == direction).unwrap();
1343            assert_eq!(side.1, "none");
1344        }
1345    }
1346
1347    #[test]
1348    fn test_state_id_from_properties_empty() {
1349        let registry = create_test_registry();
1350        let key = Identifier::vanilla_static("redstone_wire");
1351
1352        // Empty properties - should get base state with all defaults at index 0
1353        let state_id = registry
1354            .state_id_from_properties(&key, &[])
1355            .expect("Should find state");
1356
1357        let retrieved = registry.get_properties(state_id);
1358
1359        // All should be at index 0
1360        for (name, value) in &retrieved {
1361            match *name {
1362                "east" | "north" | "south" | "west" => {
1363                    assert_eq!(*value, "up", "Empty props should use index 0 = 'up'");
1364                }
1365                "power" => {
1366                    assert_eq!(*value, "0", "Empty props should use index 0 = '0'");
1367                }
1368                _ => {}
1369            }
1370        }
1371    }
1372
1373    #[test]
1374    fn test_state_id_from_properties_invalid_block() {
1375        let registry = create_test_registry();
1376        let key = Identifier::vanilla_static("nonexistent_block");
1377
1378        let result = registry.state_id_from_properties(&key, &[]);
1379        assert!(result.is_none(), "Should return None for invalid block");
1380    }
1381
1382    #[test]
1383    fn test_state_id_from_properties_invalid_property() {
1384        let registry = create_test_registry();
1385        let key = Identifier::vanilla_static("redstone_wire");
1386
1387        let invalid_props = [("invalid_property", "value")];
1388        let result = registry.state_id_from_properties(&key, &invalid_props);
1389        assert!(result.is_none(), "Should return None for invalid property");
1390    }
1391
1392    #[test]
1393    fn test_state_id_from_properties_invalid_value() {
1394        let registry = create_test_registry();
1395        let key = Identifier::vanilla_static("redstone_wire");
1396
1397        let invalid_props = [("power", "999")]; // Power only goes 0-15
1398        let result = registry.state_id_from_properties(&key, &invalid_props);
1399        assert!(result.is_none(), "Should return None for invalid value");
1400    }
1401
1402    #[test]
1403    fn same_named_direction_properties_translate_by_value_name() {
1404        let registry = create_test_registry();
1405        let wall_torch = registry.get_default_state_id(&vanilla_blocks::WALL_TORCH);
1406
1407        let south_torch = registry.set_property(
1408            wall_torch,
1409            &BlockStateProperties::HORIZONTAL_FACING,
1410            Direction::South,
1411        );
1412        let facing_from_six_way_property =
1413            registry.try_get_property(south_torch, &BlockStateProperties::FACING);
1414        assert_eq!(facing_from_six_way_property, Some(Direction::South));
1415
1416        let west_torch =
1417            registry.set_property(south_torch, &BlockStateProperties::FACING, Direction::West);
1418        let facing_from_horizontal_property =
1419            registry.try_get_property(west_torch, &BlockStateProperties::HORIZONTAL_FACING);
1420        assert_eq!(facing_from_horizontal_property, Some(Direction::West));
1421
1422        let dispenser = registry.get_default_state_id(&vanilla_blocks::DISPENSER);
1423        let upward_dispenser =
1424            registry.set_property(dispenser, &BlockStateProperties::FACING, Direction::Up);
1425        let horizontal_facing =
1426            registry.try_get_property(upward_dispenser, &BlockStateProperties::HORIZONTAL_FACING);
1427        assert_eq!(horizontal_facing, None);
1428    }
1429
1430    #[test]
1431    fn test_state_id_from_properties_rejects_properties_on_propertyless_block() {
1432        let registry = create_test_registry();
1433        let key = Identifier::vanilla_static("stone");
1434        let stone = registry.by_key(&key).expect("stone should exist");
1435
1436        let result = registry.state_id_from_properties(&key, &[("power", "1")]);
1437        assert!(
1438            result.is_none(),
1439            "Should return None for invalid property on propertyless block"
1440        );
1441
1442        let result = registry.state_id_from_block_defaulted_properties(stone, [("power", "1")]);
1443        assert!(
1444            result.is_none(),
1445            "Should return None for invalid defaulted property on propertyless block"
1446        );
1447    }
1448
1449    #[test]
1450    fn test_stone_no_properties() {
1451        let registry = create_test_registry();
1452        let key = Identifier::vanilla_static("stone");
1453
1454        // Stone has no properties
1455        let stone = registry.by_key(&key).expect("stone should exist");
1456        assert!(stone.properties.is_empty());
1457
1458        // Should still work with empty properties
1459        let state_id = registry
1460            .state_id_from_properties(&key, &[])
1461            .expect("Should find state");
1462
1463        let retrieved = registry.get_properties(state_id);
1464        assert_eq!(retrieved.len(), 0);
1465    }
1466
1467    #[test]
1468    fn test_all_redstone_power_levels() {
1469        let registry = create_test_registry();
1470        let key = Identifier::vanilla_static("redstone_wire");
1471
1472        // Test all 16 power levels
1473        for power in 0..=15 {
1474            let power_str = power.to_string();
1475            let props = [("power", power_str.as_str())];
1476
1477            let state_id = registry
1478                .state_id_from_properties(&key, &props)
1479                .unwrap_or_else(|| panic!("Should find state for power {power}"));
1480
1481            let retrieved = registry.get_properties(state_id);
1482            let found_power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1483            assert_eq!(
1484                found_power.1,
1485                power_str.as_str(),
1486                "Power level {power} mismatch"
1487            );
1488        }
1489    }
1490
1491    #[test]
1492    #[cfg(feature = "minecraft-src")]
1493    fn test_all_block_state_ids_match_minecraft() {
1494        use rustc_hash::FxHashMap as HashMap;
1495        use std::fs;
1496
1497        #[derive(serde::Deserialize)]
1498        struct BlockState {
1499            id: u16,
1500            #[serde(default)]
1501            properties: HashMap<String, String>,
1502            #[serde(default)]
1503            default: bool,
1504        }
1505
1506        #[derive(serde::Deserialize)]
1507        struct BlockData {
1508            states: Vec<BlockState>,
1509        }
1510
1511        // Try multiple paths to find blocks.json
1512        let possible_paths = [
1513            "minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1514            "../minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1515        ];
1516        let json_content = possible_paths
1517            .iter()
1518            .find_map(|path| fs::read_to_string(path).ok())
1519            .expect("Failed to read blocks.json - make sure minecraft-src is available");
1520        let blocks: HashMap<String, BlockData> =
1521            serde_json::from_str(&json_content).expect("Failed to parse blocks.json");
1522
1523        let registry = create_test_registry();
1524        let mut errors = Vec::new();
1525
1526        for (block_name, block_data) in &blocks {
1527            // Strip "minecraft:" prefix
1528            let key = Identifier::vanilla_static(
1529                block_name
1530                    .strip_prefix("minecraft:")
1531                    .unwrap_or(block_name)
1532                    .to_string()
1533                    .leak(),
1534            );
1535
1536            let Some(block) = registry.by_key(&key) else {
1537                errors.push(format!("Block {block_name} not found in registry"));
1538                continue;
1539            };
1540
1541            // Verify default state
1542            for state in &block_data.states {
1543                if state.default {
1544                    let our_default = registry.get_default_state_id(block);
1545                    if our_default.0 != state.id {
1546                        errors.push(format!(
1547                            "{}: default state mismatch - expected {}, got {}",
1548                            block_name, state.id, our_default.0
1549                        ));
1550                    }
1551                }
1552            }
1553
1554            // Verify all states
1555            for state in &block_data.states {
1556                let props: Vec<(&str, &str)> = state
1557                    .properties
1558                    .iter()
1559                    .map(|(k, v)| (k.as_str(), v.as_str()))
1560                    .collect();
1561
1562                let Some(our_state_id) = registry.state_id_from_properties(&key, &props) else {
1563                    errors.push(format!(
1564                        "{block_name}: failed to get state for properties {props:?}"
1565                    ));
1566                    continue;
1567                };
1568
1569                if our_state_id.0 != state.id {
1570                    errors.push(format!(
1571                        "{}: state mismatch for {:?} - expected {}, got {}",
1572                        block_name, props, state.id, our_state_id.0
1573                    ));
1574                }
1575            }
1576        }
1577
1578        if !errors.is_empty() {
1579            // Print first 20 errors for readability
1580            let display_errors: String = errors
1581                .iter()
1582                .take(20)
1583                .cloned()
1584                .collect::<Vec<_>>()
1585                .join("\n");
1586            panic!(
1587                "Found {} state ID mismatches:\n{}{}",
1588                errors.len(),
1589                display_errors,
1590                if errors.len() > 20 {
1591                    format!("\n... and {} more", errors.len() - 20)
1592                } else {
1593                    String::new()
1594                }
1595            );
1596        }
1597    }
1598}