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::{slice, 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
478struct StateProperties {
479    properties: slice::Iter<'static, &'static dyn Property>,
480    offset: u16,
481    stride: u16,
482}
483
484impl Iterator for StateProperties {
485    type Item = (&'static str, &'static str);
486
487    fn next(&mut self) -> Option<Self::Item> {
488        let property = *self.properties.next()?;
489        let count = property.value_count() as u16;
490        self.stride /= count;
491        let value_index = usize::from(self.offset / self.stride % count);
492        Some((
493            property.get_name(),
494            property.value_name_from_index(value_index),
495        ))
496    }
497
498    fn size_hint(&self) -> (usize, Option<usize>) {
499        self.properties.size_hint()
500    }
501}
502
503impl ExactSizeIterator for StateProperties {}
504
505impl BlockRegistry {
506    // Creates a new, empty registry.
507    #[must_use]
508    pub fn new() -> Self {
509        Self {
510            blocks_by_id: Vec::new(),
511            blocks_by_key: FxHashMap::default(),
512            tags: RegistryTags::default(),
513            allows_registering: true,
514            state_to_block_lookup: Vec::new(),
515            state_to_block_id: Vec::new(),
516            state_ticking_metadata: Vec::new(),
517            block_to_base_state: Vec::new(),
518            next_state_id: 0,
519        }
520    }
521
522    pub fn register(&mut self, block: BlockRef) -> usize {
523        assert!(
524            self.allows_registering,
525            "Cannot register blocks after the registry has been frozen"
526        );
527
528        let id = self.blocks_by_id.len();
529        let base_state_id = self.next_state_id;
530
531        let cached = block.id.get_or_init(|| id);
532        assert_eq!(*cached, id, "block registered with conflicting id");
533        self.blocks_by_key.insert(block.key.clone(), id);
534        self.blocks_by_id.push(block);
535        self.block_to_base_state.push(base_state_id);
536
537        let state_count = block.state_count();
538        for offset in 0..state_count {
539            self.state_to_block_lookup.push(block);
540            self.state_to_block_id.push(id);
541            self.state_ticking_metadata
542                .push(block.get_ticking_metadata(offset));
543        }
544
545        self.next_state_id += state_count;
546
547        id
548    }
549
550    fn try_block_index(&self, block: BlockRef) -> Option<usize> {
551        if let Some(id) = block.id.get().copied()
552            && self
553                .blocks_by_id
554                .get(id)
555                .is_some_and(|registered| *registered == block)
556        {
557            return Some(id);
558        }
559
560        self.blocks_by_key.get(&block.key).copied()
561    }
562
563    fn block_index(&self, block: BlockRef) -> usize {
564        let Some(id) = self.try_block_index(block) else {
565            panic!("Block not found");
566        };
567        id
568    }
569
570    #[must_use]
571    pub fn get_base_state_id(&self, block: BlockRef) -> BlockStateId {
572        let id = self.block_index(block);
573        BlockStateId(self.block_to_base_state[id])
574    }
575
576    /// Gets the default state ID for a block (base state + default offset)
577    #[must_use]
578    pub fn get_default_state_id(&self, block: BlockRef) -> BlockStateId {
579        let id = self.block_index(block);
580        let base = self.block_to_base_state[id];
581        BlockStateId(base + block.default_state_offset)
582    }
583
584    #[must_use]
585    pub fn by_state_id(&self, state_id: BlockStateId) -> Option<BlockRef> {
586        self.state_to_block_lookup.get(state_id.0 as usize).copied()
587    }
588
589    #[must_use]
590    pub fn get_ticking_metadata(
591        &self,
592        state_id: BlockStateId,
593    ) -> Option<BlockStateTickingMetadata> {
594        self.state_ticking_metadata
595            .get(state_id.0 as usize)
596            .copied()
597    }
598
599    /// Iterates over property names and values in the block property order without allocating
600    ///
601    /// # Panics
602    /// Panics if the state ID is invalid
603    #[inline]
604    pub fn properties(
605        &self,
606        id: BlockStateId,
607    ) -> impl ExactSizeIterator<Item = (&'static str, &'static str)> + '_ {
608        let Some(block) = self.by_state_id(id) else {
609            panic!("Invalid state ID");
610        };
611        if block.properties.is_empty() {
612            return StateProperties {
613                properties: block.properties.iter(),
614                offset: 0,
615                stride: 1,
616            };
617        }
618        let block_id = self.state_to_block_id[id.0 as usize];
619        let base_state_id = self.block_to_base_state[block_id];
620        let relative_index = id.0 - base_state_id;
621        StateProperties {
622            properties: block.properties.iter(),
623            offset: relative_index,
624            stride: block.state_count(),
625        }
626    }
627
628    /// Gets a named property serialized value without decoding the other property values
629    ///
630    /// # Panics
631    /// Panics if the state ID is invalid
632    #[must_use]
633    #[inline]
634    pub fn get_property_str(&self, id: BlockStateId, name: &str) -> Option<&'static str> {
635        let Some(block) = self.by_state_id(id) else {
636            panic!("Invalid state ID");
637        };
638        if block.properties.is_empty() {
639            return None;
640        }
641        self.find_property_str(block, id, name)
642    }
643
644    fn find_property_str(
645        &self,
646        block: BlockRef,
647        id: BlockStateId,
648        name: &str,
649    ) -> Option<&'static str> {
650        let property_index = block
651            .properties
652            .iter()
653            .position(|property| property.get_name() == name)?;
654        let property = block.properties[property_index];
655        let block_id = self.state_to_block_id[id.0 as usize];
656        let relative_index = id.0 - self.block_to_base_state[block_id];
657        let stride = Self::property_stride(block, property_index);
658        let value_index = usize::from(relative_index / stride % property.value_count() as u16);
659        Some(property.value_name_from_index(value_index))
660    }
661
662    /// Gets the state ID for a block with the given properties.
663    ///
664    /// Returns `None` if the block key is unknown or if any property name/value is invalid.
665    ///
666    /// Properties can be provided in any order. Missing properties will use the block's
667    /// default values (typically index 0 for each property).
668    #[must_use]
669    pub fn state_id_from_properties(
670        &self,
671        key: &Identifier,
672        properties: &[(&str, &str)],
673    ) -> Option<BlockStateId> {
674        let block = self.by_key(key)?;
675        self.state_id_from_block_properties(block, properties)
676    }
677
678    /// Gets the state ID for a block with the given properties.
679    ///
680    /// Returns `None` if the block is not registered or if any property
681    /// name/value is invalid.
682    #[must_use]
683    pub fn state_id_from_block_properties(
684        &self,
685        block: BlockRef,
686        properties: &[(&str, &str)],
687    ) -> Option<BlockStateId> {
688        let block_id = self.try_block_index(block)?;
689        let base_state_id = self.block_to_base_state[block_id];
690
691        let mut property_indices = vec![0usize; block.properties.len()];
692        Self::apply_property_overrides(block, &mut property_indices, properties.iter().copied())?;
693
694        Some(BlockStateId(
695            base_state_id + Self::encode_property_indices(block, &property_indices),
696        ))
697    }
698
699    /// Gets the state ID for a block by applying properties over that block's
700    /// registered default state.
701    ///
702    /// Returns `None` if the block is not registered or if any property
703    /// name/value is invalid.
704    #[must_use]
705    pub fn state_id_from_block_defaulted_properties<'a>(
706        &self,
707        block: BlockRef,
708        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
709    ) -> Option<BlockStateId> {
710        let block_id = self.try_block_index(block)?;
711        let base_state_id = self.block_to_base_state[block_id];
712
713        let mut property_indices = Self::decode_property_indices(block, block.default_state_offset);
714        Self::apply_property_overrides(block, &mut property_indices, properties)?;
715
716        Some(BlockStateId(
717            base_state_id + Self::encode_property_indices(block, &property_indices),
718        ))
719    }
720
721    /// Applies one dynamically named property value to an existing state.
722    ///
723    /// Returns `None` when the state is invalid or the state's block does not
724    /// define the named property/value pair. This mirrors Vanilla's
725    /// `StateHolder.trySetValue` path used by parsed command block inputs.
726    #[must_use]
727    pub fn try_set_property_by_name(
728        &self,
729        id: BlockStateId,
730        name: &str,
731        value: &str,
732    ) -> Option<BlockStateId> {
733        let block = self.by_state_id(id)?;
734        let block_id = *self.state_to_block_id.get(id.0 as usize)?;
735        let base_state_id = *self.block_to_base_state.get(block_id)?;
736        let property_index = block
737            .properties
738            .iter()
739            .position(|property| property.get_name() == name)?;
740        let property = block.properties[property_index];
741        let new_value_index = (0..property.value_count())
742            .find(|&index| property.value_name_from_index(index) == value)?;
743        let relative_index = id.0.checked_sub(base_state_id)?;
744        let stride = Self::property_stride(block, property_index);
745        let old_value_index = usize::from(relative_index / stride % property.value_count() as u16);
746        let new_relative_index = if new_value_index >= old_value_index {
747            relative_index.checked_add((new_value_index - old_value_index) as u16 * stride)?
748        } else {
749            relative_index.checked_sub((old_value_index - new_value_index) as u16 * stride)?
750        };
751
752        Some(BlockStateId(base_state_id + new_relative_index))
753    }
754
755    /// Returns every state id of `block` whose properties match all `(name, value)` pairs
756    /// in `filter`. An empty filter yields all states of the block (vanilla's
757    /// `getStatesOfBlock`); a non-empty filter keeps only matching states (vanilla's
758    /// `BED_HEADS`-style predicate).
759    #[must_use]
760    pub fn matching_states(&self, block: BlockRef, filter: &[(&str, &str)]) -> Vec<BlockStateId> {
761        let block_id = self.block_index(block);
762        let base_state_id = self.block_to_base_state[block_id];
763
764        (0..block.state_count())
765            .map(|offset| BlockStateId(base_state_id + offset))
766            .filter(|&state_id| {
767                filter
768                    .iter()
769                    .all(|(name, value)| self.get_property_str(state_id, name) == Some(*value))
770            })
771            .collect()
772    }
773
774    fn decode_property_indices(block: BlockRef, mut offset: u16) -> Vec<usize> {
775        let mut property_indices = vec![0; block.properties.len()];
776
777        for (i, prop) in block.properties.iter().enumerate().rev() {
778            let count = prop.value_count() as u16;
779            property_indices[i] = (offset % count) as usize;
780            offset /= count;
781        }
782
783        property_indices
784    }
785
786    fn apply_property_overrides<'a>(
787        block: BlockRef,
788        property_indices: &mut [usize],
789        properties: impl IntoIterator<Item = (&'a str, &'a str)>,
790    ) -> Option<()> {
791        for (prop_name, prop_value) in properties {
792            let prop_idx = block
793                .properties
794                .iter()
795                .position(|p| p.get_name() == prop_name)?;
796
797            let prop = block.properties[prop_idx];
798            let value_idx = (0..prop.value_count())
799                .find(|&index| prop.value_name_from_index(index) == prop_value)?;
800
801            property_indices[prop_idx] = value_idx;
802        }
803
804        Some(())
805    }
806
807    fn encode_property_indices(block: BlockRef, property_indices: &[usize]) -> u16 {
808        let mut offset = 0u16;
809        let mut multiplier = 1u16;
810        for (idx, prop) in property_indices.iter().zip(block.properties.iter()).rev() {
811            offset += *idx as u16 * multiplier;
812            multiplier *= prop.value_count() as u16;
813        }
814
815        offset
816    }
817
818    fn property_stride(block: BlockRef, property_index: usize) -> u16 {
819        block.properties[property_index + 1..]
820            .iter()
821            .map(|property| property.value_count() as u16)
822            .product()
823    }
824
825    // Panics if that property isn't supposed to be on this block.
826    pub fn get_property<P: Property>(&self, id: BlockStateId, property: &P) -> P::Value {
827        self.try_get_property(id, property)
828            .expect("Property not found on this block")
829    }
830
831    /// Gets the value of a property, returning `None` if the block doesn't have this property.
832    #[must_use]
833    pub fn try_get_property<P: Property>(
834        &self,
835        id: BlockStateId,
836        property: &P,
837    ) -> Option<P::Value> {
838        let block = self.by_state_id(id).expect("Invalid state ID");
839
840        // Find the property index in the block's property list
841        let property_index = block
842            .properties
843            .iter()
844            .position(|prop| prop.get_name() == property.get_name())?;
845
846        // Get the base state ID for this block (O(1) lookup)
847        let block_id = self.state_to_block_id[id.0 as usize];
848        let base_state_id = self.block_to_base_state[block_id];
849
850        // Calculate the relative state index
851        let relative_index = id.0 - base_state_id;
852
853        let block_property = block.properties[property_index];
854        let stride = Self::property_stride(block, property_index);
855        let value_index =
856            usize::from(relative_index / stride % block_property.value_count() as u16);
857        let block_value = block_property.value_name_from_index(value_index);
858
859        property.get_value(block_value)
860    }
861
862    // Panics if that property isn't supposed to be on this block.
863    pub fn set_property<P: Property>(
864        &self,
865        id: BlockStateId,
866        property: &P,
867        value: P::Value,
868    ) -> BlockStateId {
869        let block = self.by_state_id(id).expect("Invalid state ID");
870
871        // Find the property index in the block's property list
872        let property_index = block
873            .properties
874            .iter()
875            .position(|prop| prop.get_name() == property.get_name())
876            .unwrap_or_else(|| {
877                panic!(
878                    "Property {} not found on block {}",
879                    property.get_name(),
880                    block.key
881                )
882            });
883
884        // Get the base state ID for this block (O(1) lookup)
885        let block_id = self.state_to_block_id[id.0 as usize];
886        let base_state_id = self.block_to_base_state[block_id];
887
888        // Calculate the relative state index
889        let relative_index = id.0 - base_state_id;
890
891        let caller_value_index = property.get_internal_index(&value);
892        let value_name = property.value_name_from_index(caller_value_index);
893        let block_property = block.properties[property_index];
894        let Some(new_value_index) = (0..block_property.value_count())
895            .find(|&index| block_property.value_name_from_index(index) == value_name)
896        else {
897            panic!(
898                "Value {} for property {} not found on block {}",
899                value_name,
900                property.get_name(),
901                block.key
902            );
903        };
904        let stride = Self::property_stride(block, property_index);
905        let old_value_index =
906            usize::from(relative_index / stride % block_property.value_count() as u16);
907        let new_relative_index = if new_value_index >= old_value_index {
908            relative_index + (new_value_index - old_value_index) as u16 * stride
909        } else {
910            relative_index - (old_value_index - new_value_index) as u16 * stride
911        };
912
913        BlockStateId(base_state_id + new_relative_index)
914    }
915
916    pub fn iter(&self) -> impl Iterator<Item = (usize, BlockRef)> + '_ {
917        self.blocks_by_id
918            .iter()
919            .enumerate()
920            .map(|(id, &block)| (id, block))
921    }
922}
923
924crate::impl_registry_ext!(BlockRegistry, Block, blocks_by_id, blocks_by_key);
925
926crate::impl_registry_entry_eq!(Block);
927
928impl crate::RegistryEntry for Block {
929    fn key(&self) -> &Identifier {
930        &self.key
931    }
932
933    fn try_id(&self) -> Option<usize> {
934        self.id.get().copied()
935    }
936}
937crate::impl_tagged_registry!(BlockRegistry, blocks_by_key, "block");
938
939// Shape lookup methods
940impl BlockRegistry {
941    fn block_and_state_offset(&self, state_id: BlockStateId) -> Option<(BlockRef, u16)> {
942        let block = self
943            .state_to_block_lookup
944            .get(state_id.0 as usize)
945            .copied()?;
946        let block_id = self
947            .state_to_block_id
948            .get(state_id.0 as usize)
949            .copied()
950            .unwrap_or(0);
951        let base_state = self.block_to_base_state.get(block_id).copied().unwrap_or(0);
952        let offset = state_id.0.saturating_sub(base_state);
953        Some((block, offset))
954    }
955
956    fn static_shape_for_state(
957        &self,
958        state_id: BlockStateId,
959        shape: fn(&Block, u16) -> shapes::VoxelShape,
960    ) -> shapes::VoxelShape {
961        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
962            return shapes::VoxelShape::FULL_BLOCK;
963        };
964        shape(block, offset)
965    }
966
967    fn offset_shape_for_state(
968        &self,
969        state_id: BlockStateId,
970        pos: BlockPos,
971        channel: ShapeChannel,
972        shape: fn(&Block, u16) -> shapes::VoxelShape,
973    ) -> shapes::OffsetVoxelShape {
974        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
975            return shapes::OffsetVoxelShape::without_offset(shapes::VoxelShape::FULL_BLOCK);
976        };
977
978        let shape = shape(block, offset);
979        let offset = if block.shape_offsets.uses_offset(channel) {
980            block.offset_at(pos)
981        } else {
982            DVec3::ZERO
983        };
984        shapes::OffsetVoxelShape::new(shape, offset)
985    }
986
987    /// Gets the collision shape for a block state.
988    ///
989    /// For simple blocks this is typically a single full-block box.
990    /// For complex blocks like fences, this may be multiple boxes.
991    #[must_use]
992    pub fn get_static_collision_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
993        self.static_shape_for_state(state_id, Block::get_collision_shape)
994    }
995
996    #[must_use]
997    pub fn is_suffocating(&self, state_id: BlockStateId) -> bool {
998        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
999            return false;
1000        };
1001        block.suffocating.value(offset)
1002    }
1003
1004    /// Returns the extracted static `BlockState.isRedstoneConductor` value.
1005    ///
1006    /// Dynamic block behaviors may override this value using the live level and position.
1007    #[must_use]
1008    pub fn is_static_redstone_conductor(&self, state_id: BlockStateId) -> bool {
1009        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
1010            return false;
1011        };
1012        block.redstone_conductor.value(offset)
1013    }
1014
1015    #[must_use]
1016    pub fn get_collision_shape_at(
1017        &self,
1018        state_id: BlockStateId,
1019        pos: BlockPos,
1020    ) -> shapes::OffsetVoxelShape {
1021        self.offset_shape_for_state(
1022            state_id,
1023            pos,
1024            ShapeChannel::Collision,
1025            Block::get_collision_shape,
1026        )
1027    }
1028
1029    /// Gets the block support shape for a block state.
1030    ///
1031    /// Vanilla support checks use `BlockState.getBlockSupportShape`, not collision shape,
1032    /// for `isFaceSturdy` and multiface side attachment.
1033    #[must_use]
1034    pub fn get_static_support_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1035        self.static_shape_for_state(state_id, Block::get_support_shape)
1036    }
1037
1038    #[must_use]
1039    pub fn get_support_shape_at(
1040        &self,
1041        state_id: BlockStateId,
1042        pos: BlockPos,
1043    ) -> shapes::OffsetVoxelShape {
1044        self.offset_shape_for_state(
1045            state_id,
1046            pos,
1047            ShapeChannel::Support,
1048            Block::get_support_shape,
1049        )
1050    }
1051
1052    /// Gets the outline shape for a block state.
1053    ///
1054    /// This is the shape shown when the player targets the block.
1055    /// Often the same as collision shape, but can differ (e.g., fences).
1056    #[must_use]
1057    pub fn get_static_outline_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1058        self.static_shape_for_state(state_id, Block::get_outline_shape)
1059    }
1060
1061    #[must_use]
1062    pub fn get_outline_shape_at(
1063        &self,
1064        state_id: BlockStateId,
1065        pos: BlockPos,
1066    ) -> shapes::OffsetVoxelShape {
1067        self.offset_shape_for_state(
1068            state_id,
1069            pos,
1070            ShapeChannel::Outline,
1071            Block::get_outline_shape,
1072        )
1073    }
1074
1075    /// Gets the occlusion shape for a block state.
1076    ///
1077    /// Vanilla caches this as `BlockState.getOcclusionShape()` and uses it for
1078    /// `isSolidRender`, light occlusion, and face occlusion.
1079    #[must_use]
1080    pub fn get_occlusion_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1081        self.static_shape_for_state(state_id, Block::get_occlusion_shape)
1082    }
1083
1084    /// Gets the interaction shape for a block state.
1085    ///
1086    /// Vanilla uses this as an interaction hit override after the primary raycast
1087    /// shape has already hit.
1088    #[must_use]
1089    pub fn get_static_interaction_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1090        self.static_shape_for_state(state_id, Block::get_interaction_shape)
1091    }
1092
1093    #[must_use]
1094    pub fn get_interaction_shape_at(
1095        &self,
1096        state_id: BlockStateId,
1097        pos: BlockPos,
1098    ) -> shapes::OffsetVoxelShape {
1099        self.offset_shape_for_state(
1100            state_id,
1101            pos,
1102            ShapeChannel::Interaction,
1103            Block::get_interaction_shape,
1104        )
1105    }
1106
1107    /// Gets the visual shape for a block state.
1108    ///
1109    /// Vanilla uses this for visual raycasts; it defaults to collision shape but
1110    /// differs for a few blocks such as fences, mud, soul sand, and powder snow.
1111    #[must_use]
1112    pub fn get_static_visual_shape(&self, state_id: BlockStateId) -> shapes::VoxelShape {
1113        self.static_shape_for_state(state_id, Block::get_visual_shape)
1114    }
1115
1116    #[must_use]
1117    pub fn get_visual_shape_at(
1118        &self,
1119        state_id: BlockStateId,
1120        pos: BlockPos,
1121    ) -> shapes::OffsetVoxelShape {
1122        self.offset_shape_for_state(state_id, pos, ShapeChannel::Visual, Block::get_visual_shape)
1123    }
1124
1125    /// Gets all static shape channels for a block state.
1126    #[must_use]
1127    pub fn get_static_shapes(&self, state_id: BlockStateId) -> shapes::BlockShapes {
1128        shapes::BlockShapes::new(
1129            self.get_static_collision_shape(state_id),
1130            self.get_static_support_shape(state_id),
1131            self.get_static_outline_shape(state_id),
1132            self.get_occlusion_shape(state_id),
1133            self.get_static_interaction_shape(state_id),
1134            self.get_static_visual_shape(state_id),
1135        )
1136    }
1137
1138    #[must_use]
1139    pub fn get_light_properties(&self, state_id: BlockStateId) -> BlockLightProperties {
1140        let Some((block, offset)) = self.block_and_state_offset(state_id) else {
1141            return BlockLightProperties::OPAQUE_FULL_BLOCK;
1142        };
1143        block.get_light_properties(offset)
1144    }
1145
1146    pub fn copy_matching_properties(&self, source: BlockStateId, target: BlockRef) -> BlockStateId {
1147        self.with_properties_of(self.get_default_state_id(target), source)
1148    }
1149
1150    /// Copies matching properties preserving other target values
1151    #[must_use]
1152    pub fn with_properties_of(&self, target: BlockStateId, source: BlockStateId) -> BlockStateId {
1153        let Some((target_block, target_offset)) = self.block_and_state_offset(target) else {
1154            panic!("cannot copy properties onto invalid block state {target:?}");
1155        };
1156
1157        let Some((source_block, source_offset)) = self.block_and_state_offset(source) else {
1158            panic!("cannot copy properties from invalid block state {source:?}");
1159        };
1160
1161        let mut target_indices = Self::decode_property_indices(target_block, target_offset);
1162        let source_indices = Self::decode_property_indices(source_block, source_offset);
1163
1164        for (target_index, target_property) in target_block.properties.iter().enumerate() {
1165            let Some(source_index) = source_block.properties.iter().position(|source_property| {
1166                source_property.get_name() == target_property.get_name()
1167                    && source_property.value_count() == target_property.value_count()
1168                    && (0..target_property.value_count()).all(|index| {
1169                        source_property.value_name_from_index(index)
1170                            == target_property.value_name_from_index(index)
1171                    })
1172            }) else {
1173                continue;
1174            };
1175
1176            target_indices[target_index] = source_indices[source_index];
1177        }
1178
1179        BlockStateId(
1180            target.0 - target_offset + Self::encode_property_indices(target_block, &target_indices),
1181        )
1182    }
1183}
1184
1185/// Macro to generate offset calculation from property values in all positions.
1186///
1187/// Takes property objects and their values, automatically converts to indices.
1188/// All properties must be specified in order.
1189///
1190/// # Note
1191/// For boolean properties, use `.index_of(value)` to handle the inverted encoding
1192/// (true=0, false=1 for Java compatibility).
1193///
1194/// # Example
1195/// ```ignore
1196/// use steel_registry::{offset, properties::{BlockStateProperties as Props, RedstoneSide}};
1197///
1198/// const WIRE: Block = Block::new("wire", behavior, PROPS)
1199///     .with_default_state(offset!(
1200///         Props::EAST_REDSTONE => RedstoneSide::Up,
1201///         Props::NORTH_REDSTONE => RedstoneSide::None,
1202///         Props::POWER => 10,
1203///         Props::ATTACHED => Props::ATTACHED.index_of(false)  // Bools need .index_of()
1204///     ));
1205/// ```
1206#[macro_export]
1207macro_rules! offset {
1208    ($($prop:expr => $value:expr),* $(,)?) => {{
1209        const INDICES: &[usize] = &[$($value as usize),*];
1210        const COUNTS: &[usize] = &[$($prop.value_count()),*];
1211        $crate::blocks::Block::calculate_offset(INDICES, COUNTS)
1212    }};
1213}
1214
1215/// Re-export for easier access
1216pub use offset;
1217use steel_utils::Identifier;
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222    use crate::blocks::properties::{BlockStateProperties, Direction};
1223    use crate::vanilla_blocks;
1224
1225    fn create_test_registry() -> BlockRegistry {
1226        let mut registry = BlockRegistry::new();
1227        vanilla_blocks::register_blocks(&mut registry);
1228        registry.freeze();
1229        registry
1230    }
1231
1232    #[test]
1233    fn tag_modification_keeps_order_and_membership_in_sync() {
1234        let mut registry = BlockRegistry::new();
1235        vanilla_blocks::register_blocks(&mut registry);
1236        let tag = Identifier::new_static("test", "ordered_membership");
1237        registry.register_tag(
1238            Identifier::new_static("test", "ordered_membership"),
1239            &["stone", "dirt"],
1240        );
1241
1242        assert!(registry.is_in_tag(&vanilla_blocks::STONE, &tag));
1243        assert_eq!(
1244            registry
1245                .iter_tag(&tag)
1246                .map(|block| block.key.path.as_ref())
1247                .collect::<Vec<_>>(),
1248            ["stone", "dirt"]
1249        );
1250
1251        registry.modify_tag(&tag, |_| {
1252            vec![
1253                Identifier::vanilla_static("oak_log"),
1254                Identifier::vanilla_static("dirt"),
1255            ]
1256        });
1257        registry.freeze();
1258
1259        assert!(!registry.is_in_tag(&vanilla_blocks::STONE, &tag));
1260        assert!(registry.is_in_tag(&vanilla_blocks::OAK_LOG, &tag));
1261        assert!(registry.is_in_tag(&vanilla_blocks::DIRT, &tag));
1262        assert_eq!(
1263            registry
1264                .iter_tag(&tag)
1265                .map(|block| block.key.path.as_ref())
1266                .collect::<Vec<_>>(),
1267            ["oak_log", "dirt"]
1268        );
1269    }
1270
1271    #[test]
1272    fn test_redstone_wire_properties() {
1273        let registry = create_test_registry();
1274        let redstone_wire = registry
1275            .by_key(&Identifier::vanilla_static("redstone_wire"))
1276            .expect("redstone_wire should exist");
1277
1278        // Redstone wire has 5 properties
1279        assert_eq!(redstone_wire.properties.len(), 5);
1280
1281        // Check property names
1282        let prop_names: Vec<&str> = redstone_wire
1283            .properties
1284            .iter()
1285            .map(|p| p.get_name())
1286            .collect();
1287        assert!(prop_names.contains(&"east"));
1288        assert!(prop_names.contains(&"north"));
1289        assert!(prop_names.contains(&"south"));
1290        assert!(prop_names.contains(&"west"));
1291        assert!(prop_names.contains(&"power"));
1292    }
1293
1294    #[test]
1295    fn test_redstone_wire_state_count() {
1296        let registry = create_test_registry();
1297
1298        // Redstone wire: 3 sides × 3 sides × 3 sides × 3 sides × 16 power levels = 1296 states
1299        // Actually checking the state count
1300        let redstone_wire = registry
1301            .by_key(&Identifier::vanilla_static("redstone_wire"))
1302            .expect("redstone_wire should exist");
1303
1304        let mut state_count = 1;
1305        for prop in redstone_wire.properties {
1306            state_count *= prop.get_possible_value_names().len();
1307        }
1308        assert_eq!(state_count, 3 * 3 * 3 * 3 * 16); // 1296
1309    }
1310
1311    #[test]
1312    fn property_iterator_preserves_mixed_radix_values_when_skipping() {
1313        let registry = create_test_registry();
1314        let block = registry
1315            .by_key(&Identifier::vanilla_static("oak_stairs"))
1316            .expect("oak_stairs should exist");
1317        let base = registry.get_base_state_id(block);
1318
1319        for offset in 0..block.state_count() {
1320            let state = BlockStateId(base.0 + offset);
1321            let indices = BlockRegistry::decode_property_indices(block, offset);
1322            let expected = block
1323                .properties
1324                .iter()
1325                .zip(indices)
1326                .map(|(property, index)| {
1327                    (property.get_name(), property.value_name_from_index(index))
1328                })
1329                .collect::<Vec<_>>();
1330
1331            assert_eq!(registry.properties(state).collect::<Vec<_>>(), expected);
1332            for (skip, &value) in expected.iter().enumerate() {
1333                assert_eq!(registry.properties(state).nth(skip), Some(value));
1334            }
1335            assert_eq!(registry.properties(state).last(), expected.last().copied());
1336        }
1337    }
1338
1339    #[test]
1340    fn test_properties_default_state() {
1341        let registry = create_test_registry();
1342        let redstone_wire = registry
1343            .by_key(&Identifier::vanilla_static("redstone_wire"))
1344            .expect("redstone_wire should exist");
1345
1346        let default_state = registry.get_default_state_id(redstone_wire);
1347        let properties = registry.properties(default_state).collect::<Vec<_>>();
1348
1349        // Default state should have all sides "none" and power 0
1350        assert_eq!(properties.len(), 5);
1351
1352        for (name, value) in &properties {
1353            match *name {
1354                "east" | "north" | "south" | "west" => {
1355                    assert_eq!(*value, "none", "Default side should be 'none'");
1356                }
1357                "power" => {
1358                    assert_eq!(*value, "0", "Default power should be '0'");
1359                }
1360                _ => panic!("Unexpected property: {name}"),
1361            }
1362        }
1363    }
1364
1365    #[test]
1366    fn test_state_id_from_properties_roundtrip() {
1367        let registry = create_test_registry();
1368        let key = Identifier::vanilla_static("redstone_wire");
1369
1370        // Test with specific properties
1371        let properties = [
1372            ("east", "up"),
1373            ("north", "side"),
1374            ("south", "none"),
1375            ("west", "up"),
1376            ("power", "15"),
1377        ];
1378
1379        let state_id = registry
1380            .state_id_from_properties(&key, &properties)
1381            .expect("Should find state");
1382
1383        // Get properties back and verify
1384        let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1385        assert_eq!(retrieved.len(), 5);
1386
1387        for (name, value) in &properties {
1388            let found = retrieved
1389                .iter()
1390                .find(|(n, _)| n == name)
1391                .expect("Property should exist");
1392            assert_eq!(found.1, *value, "Property {name} mismatch");
1393        }
1394    }
1395
1396    #[test]
1397    fn test_state_id_from_properties_partial() {
1398        let registry = create_test_registry();
1399        let key = Identifier::vanilla_static("redstone_wire");
1400
1401        // Only specify some properties - others should default to index 0
1402        let partial_props = [("power", "10"), ("east", "side")];
1403
1404        let state_id = registry
1405            .state_id_from_properties(&key, &partial_props)
1406            .expect("Should find state");
1407
1408        let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1409
1410        // Verify specified properties
1411        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1412        assert_eq!(power.1, "10");
1413
1414        let east = retrieved.iter().find(|(n, _)| *n == "east").unwrap();
1415        assert_eq!(east.1, "side");
1416
1417        // Unspecified properties should be at index 0 (first value in enum)
1418        let north = retrieved.iter().find(|(n, _)| *n == "north").unwrap();
1419        assert_eq!(north.1, "up"); // Index 0 is "up" for RedstoneSide
1420    }
1421
1422    #[test]
1423    fn dynamically_named_property_update_preserves_other_state_values() {
1424        let registry = create_test_registry();
1425        let wire = registry
1426            .state_id_from_block_defaulted_properties(
1427                &vanilla_blocks::REDSTONE_WIRE,
1428                [("east", "side"), ("power", "7")],
1429            )
1430            .expect("redstone wire state should exist");
1431
1432        let updated = registry
1433            .try_set_property_by_name(wire, "east", "up")
1434            .expect("dynamic property should update");
1435
1436        let properties = registry.properties(updated).collect::<Vec<_>>();
1437        assert!(properties.contains(&("east", "up")));
1438        assert!(properties.contains(&("power", "7")));
1439        assert!(
1440            registry
1441                .try_set_property_by_name(updated, "missing", "value")
1442                .is_none()
1443        );
1444    }
1445
1446    #[test]
1447    fn test_state_id_from_block_defaulted_properties_keeps_missing_defaults() {
1448        let registry = create_test_registry();
1449        let key = Identifier::vanilla_static("redstone_wire");
1450        let block = registry.by_key(&key).expect("redstone_wire should exist");
1451
1452        let state_id = registry
1453            .state_id_from_block_defaulted_properties(block, [("power", "10")])
1454            .expect("Should find state");
1455
1456        let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1457
1458        let power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1459        assert_eq!(power.1, "10");
1460
1461        for direction in ["east", "north", "south", "west"] {
1462            let side = retrieved.iter().find(|(n, _)| *n == direction).unwrap();
1463            assert_eq!(side.1, "none");
1464        }
1465    }
1466
1467    #[test]
1468    fn test_state_id_from_properties_empty() {
1469        let registry = create_test_registry();
1470        let key = Identifier::vanilla_static("redstone_wire");
1471
1472        // Empty properties - should get base state with all defaults at index 0
1473        let state_id = registry
1474            .state_id_from_properties(&key, &[])
1475            .expect("Should find state");
1476
1477        let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1478
1479        // All should be at index 0
1480        for (name, value) in &retrieved {
1481            match *name {
1482                "east" | "north" | "south" | "west" => {
1483                    assert_eq!(*value, "up", "Empty props should use index 0 = 'up'");
1484                }
1485                "power" => {
1486                    assert_eq!(*value, "0", "Empty props should use index 0 = '0'");
1487                }
1488                _ => {}
1489            }
1490        }
1491    }
1492
1493    #[test]
1494    fn test_state_id_from_properties_invalid_block() {
1495        let registry = create_test_registry();
1496        let key = Identifier::vanilla_static("nonexistent_block");
1497
1498        let result = registry.state_id_from_properties(&key, &[]);
1499        assert!(result.is_none(), "Should return None for invalid block");
1500    }
1501
1502    #[test]
1503    fn test_state_id_from_properties_invalid_property() {
1504        let registry = create_test_registry();
1505        let key = Identifier::vanilla_static("redstone_wire");
1506
1507        let invalid_props = [("invalid_property", "value")];
1508        let result = registry.state_id_from_properties(&key, &invalid_props);
1509        assert!(result.is_none(), "Should return None for invalid property");
1510    }
1511
1512    #[test]
1513    fn test_state_id_from_properties_invalid_value() {
1514        let registry = create_test_registry();
1515        let key = Identifier::vanilla_static("redstone_wire");
1516
1517        let invalid_props = [("power", "999")]; // Power only goes 0-15
1518        let result = registry.state_id_from_properties(&key, &invalid_props);
1519        assert!(result.is_none(), "Should return None for invalid value");
1520    }
1521
1522    #[test]
1523    fn same_named_direction_properties_translate_by_value_name() {
1524        let registry = create_test_registry();
1525        let wall_torch = registry.get_default_state_id(&vanilla_blocks::WALL_TORCH);
1526
1527        let south_torch = registry.set_property(
1528            wall_torch,
1529            &BlockStateProperties::HORIZONTAL_FACING,
1530            Direction::South,
1531        );
1532        let facing_from_six_way_property =
1533            registry.try_get_property(south_torch, &BlockStateProperties::FACING);
1534        assert_eq!(facing_from_six_way_property, Some(Direction::South));
1535
1536        let west_torch =
1537            registry.set_property(south_torch, &BlockStateProperties::FACING, Direction::West);
1538        let facing_from_horizontal_property =
1539            registry.try_get_property(west_torch, &BlockStateProperties::HORIZONTAL_FACING);
1540        assert_eq!(facing_from_horizontal_property, Some(Direction::West));
1541
1542        let dispenser = registry.get_default_state_id(&vanilla_blocks::DISPENSER);
1543        let upward_dispenser =
1544            registry.set_property(dispenser, &BlockStateProperties::FACING, Direction::Up);
1545        let horizontal_facing =
1546            registry.try_get_property(upward_dispenser, &BlockStateProperties::HORIZONTAL_FACING);
1547        assert_eq!(horizontal_facing, None);
1548    }
1549
1550    #[test]
1551    fn test_state_id_from_properties_rejects_properties_on_propertyless_block() {
1552        let registry = create_test_registry();
1553        let key = Identifier::vanilla_static("stone");
1554        let stone = registry.by_key(&key).expect("stone should exist");
1555
1556        let result = registry.state_id_from_properties(&key, &[("power", "1")]);
1557        assert!(
1558            result.is_none(),
1559            "Should return None for invalid property on propertyless block"
1560        );
1561
1562        let result = registry.state_id_from_block_defaulted_properties(stone, [("power", "1")]);
1563        assert!(
1564            result.is_none(),
1565            "Should return None for invalid defaulted property on propertyless block"
1566        );
1567    }
1568
1569    #[test]
1570    fn test_stone_no_properties() {
1571        let registry = create_test_registry();
1572        let key = Identifier::vanilla_static("stone");
1573
1574        // Stone has no properties
1575        let stone = registry.by_key(&key).expect("stone should exist");
1576        assert!(stone.properties.is_empty());
1577
1578        // Should still work with empty properties
1579        let state_id = registry
1580            .state_id_from_properties(&key, &[])
1581            .expect("Should find state");
1582
1583        let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1584        assert_eq!(retrieved.len(), 0);
1585    }
1586
1587    #[test]
1588    fn test_all_redstone_power_levels() {
1589        let registry = create_test_registry();
1590        let key = Identifier::vanilla_static("redstone_wire");
1591
1592        // Test all 16 power levels
1593        for power in 0..=15 {
1594            let power_str = power.to_string();
1595            let props = [("power", power_str.as_str())];
1596
1597            let state_id = registry
1598                .state_id_from_properties(&key, &props)
1599                .unwrap_or_else(|| panic!("Should find state for power {power}"));
1600
1601            let retrieved = registry.properties(state_id).collect::<Vec<_>>();
1602            let found_power = retrieved.iter().find(|(n, _)| *n == "power").unwrap();
1603            assert_eq!(
1604                found_power.1,
1605                power_str.as_str(),
1606                "Power level {power} mismatch"
1607            );
1608        }
1609    }
1610
1611    #[test]
1612    #[cfg(feature = "minecraft-src")]
1613    fn test_all_block_state_ids_match_minecraft() {
1614        use rustc_hash::FxHashMap as HashMap;
1615        use std::fs;
1616
1617        #[derive(serde::Deserialize)]
1618        struct BlockState {
1619            id: u16,
1620            #[serde(default)]
1621            properties: HashMap<String, String>,
1622            #[serde(default)]
1623            default: bool,
1624        }
1625
1626        #[derive(serde::Deserialize)]
1627        struct BlockData {
1628            states: Vec<BlockState>,
1629        }
1630
1631        // Try multiple paths to find blocks.json
1632        let possible_paths = [
1633            "minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1634            "../minecraft-src/minecraft/resources/datagen-reports/blocks.json",
1635        ];
1636        let json_content = possible_paths
1637            .iter()
1638            .find_map(|path| fs::read_to_string(path).ok())
1639            .expect("Failed to read blocks.json - make sure minecraft-src is available");
1640        let blocks: HashMap<String, BlockData> =
1641            serde_json::from_str(&json_content).expect("Failed to parse blocks.json");
1642
1643        let registry = create_test_registry();
1644        let mut errors = Vec::new();
1645
1646        for (block_name, block_data) in &blocks {
1647            // Strip "minecraft:" prefix
1648            let key = Identifier::vanilla_static(
1649                block_name
1650                    .strip_prefix("minecraft:")
1651                    .unwrap_or(block_name)
1652                    .to_string()
1653                    .leak(),
1654            );
1655
1656            let Some(block) = registry.by_key(&key) else {
1657                errors.push(format!("Block {block_name} not found in registry"));
1658                continue;
1659            };
1660
1661            // Verify default state
1662            for state in &block_data.states {
1663                if state.default {
1664                    let our_default = registry.get_default_state_id(block);
1665                    if our_default.0 != state.id {
1666                        errors.push(format!(
1667                            "{}: default state mismatch - expected {}, got {}",
1668                            block_name, state.id, our_default.0
1669                        ));
1670                    }
1671                }
1672            }
1673
1674            // Verify all states
1675            for state in &block_data.states {
1676                let props: Vec<(&str, &str)> = state
1677                    .properties
1678                    .iter()
1679                    .map(|(k, v)| (k.as_str(), v.as_str()))
1680                    .collect();
1681
1682                let Some(our_state_id) = registry.state_id_from_properties(&key, &props) else {
1683                    errors.push(format!(
1684                        "{block_name}: failed to get state for properties {props:?}"
1685                    ));
1686                    continue;
1687                };
1688
1689                if our_state_id.0 != state.id {
1690                    errors.push(format!(
1691                        "{}: state mismatch for {:?} - expected {}, got {}",
1692                        block_name, props, state.id, our_state_id.0
1693                    ));
1694                }
1695            }
1696        }
1697
1698        if !errors.is_empty() {
1699            // Print first 20 errors for readability
1700            let display_errors: String = errors
1701                .iter()
1702                .take(20)
1703                .cloned()
1704                .collect::<Vec<_>>()
1705                .join("\n");
1706            panic!(
1707                "Found {} state ID mismatches:\n{}{}",
1708                errors.len(),
1709                display_errors,
1710                if errors.len() > 20 {
1711                    format!("\n... and {} more", errors.len() - 20)
1712                } else {
1713                    String::new()
1714                }
1715            );
1716        }
1717    }
1718}