Skip to main content

steel_registry/
item_predicate.rs

1//! Vanilla item, block, state, and NBT predicate codec values.
2
3use std::cmp::Ordering;
4use std::fmt::Debug;
5use std::io::{Cursor, Error, Result, Write};
6
7use simdnbt::owned::{NbtCompound, NbtList, NbtTag, read_tag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::BlockStateId;
10use steel_utils::codec::VarInt;
11use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
12use steel_utils::nbt::{
13    NbtNumeric, normalize_nbt_compound, parse_snbt_compound, to_canonical_snbt,
14    vanilla_nbt_heap_size,
15};
16use steel_utils::serial::{ReadFrom, WriteTo};
17
18use crate::blocks::{Block, properties::Property};
19use crate::data_component_predicate::DataComponentMatchers;
20use crate::items::Item;
21use crate::{REGISTRY, RegistryHolderSet};
22
23const DEFAULT_NBT_QUOTA: u64 = 2_097_152;
24const MAX_UTF_LENGTH: usize = 32_767;
25
26/// Vanilla integer min/max bounds.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct IntBounds {
29    min: Option<i32>,
30    max: Option<i32>,
31}
32
33impl IntBounds {
34    pub const ANY: Self = Self {
35        min: None,
36        max: None,
37    };
38
39    #[must_use]
40    pub fn new(min: Option<i32>, max: Option<i32>) -> Option<Self> {
41        if min.zip(max).is_some_and(|(min, max)| min > max) {
42            return None;
43        }
44        Some(Self { min, max })
45    }
46
47    #[must_use]
48    pub const fn exactly(value: i32) -> Self {
49        Self {
50            min: Some(value),
51            max: Some(value),
52        }
53    }
54
55    #[must_use]
56    pub const fn min(&self) -> Option<i32> {
57        self.min
58    }
59
60    #[must_use]
61    pub const fn max(&self) -> Option<i32> {
62        self.max
63    }
64
65    #[must_use]
66    pub const fn is_any(&self) -> bool {
67        self.min.is_none() && self.max.is_none()
68    }
69
70    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
71        if let Some(value) = tag.codec_i32() {
72            return Some(Self::exactly(value));
73        }
74        let compound = tag.compound()?;
75        Self::new(
76            decode_optional(compound, "min", NbtNumeric::codec_i32)?,
77            decode_optional(compound, "max", NbtNumeric::codec_i32)?,
78        )
79    }
80
81    pub(crate) fn as_nbt_tag(&self) -> NbtTag {
82        if let (Some(min), Some(max)) = (self.min, self.max)
83            && min == max
84        {
85            return NbtTag::Int(min);
86        }
87        let mut compound = NbtCompound::new();
88        if let Some(min) = self.min {
89            compound.insert("min", min);
90        }
91        if let Some(max) = self.max {
92            compound.insert("max", max);
93        }
94        NbtTag::Compound(compound)
95    }
96}
97
98impl HashComponent for IntBounds {
99    fn hash_component(&self, hasher: &mut ComponentHasher) {
100        self.as_nbt_tag().hash_component(hasher);
101    }
102}
103
104/// Vanilla double min/max bounds.
105#[derive(Debug, Clone, Copy, Default)]
106pub struct DoubleBounds {
107    min: Option<f64>,
108    max: Option<f64>,
109}
110
111impl DoubleBounds {
112    pub const ANY: Self = Self {
113        min: None,
114        max: None,
115    };
116
117    #[must_use]
118    pub fn new(min: Option<f64>, max: Option<f64>) -> Option<Self> {
119        if min
120            .zip(max)
121            .is_some_and(|(min, max)| java_double_compare(min, max).is_gt())
122        {
123            return None;
124        }
125        Some(Self { min, max })
126    }
127
128    #[must_use]
129    pub const fn exactly(value: f64) -> Self {
130        Self {
131            min: Some(value),
132            max: Some(value),
133        }
134    }
135
136    #[must_use]
137    pub const fn min(&self) -> Option<f64> {
138        self.min
139    }
140
141    #[must_use]
142    pub const fn max(&self) -> Option<f64> {
143        self.max
144    }
145
146    #[must_use]
147    pub const fn is_any(&self) -> bool {
148        self.min.is_none() && self.max.is_none()
149    }
150
151    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
152        if let Some(value) = tag.codec_f64() {
153            return Some(Self::exactly(value));
154        }
155        let compound = tag.compound()?;
156        Self::new(
157            decode_optional(compound, "min", NbtNumeric::codec_f64)?,
158            decode_optional(compound, "max", NbtNumeric::codec_f64)?,
159        )
160    }
161
162    pub(crate) fn as_nbt_tag(&self) -> NbtTag {
163        if let (Some(min), Some(max)) = (self.min, self.max)
164            && java_double_equals(min, max)
165        {
166            return NbtTag::Double(min);
167        }
168        let mut compound = NbtCompound::new();
169        if let Some(min) = self.min {
170            compound.insert("min", min);
171        }
172        if let Some(max) = self.max {
173            compound.insert("max", max);
174        }
175        NbtTag::Compound(compound)
176    }
177}
178
179impl PartialEq for DoubleBounds {
180    fn eq(&self, other: &Self) -> bool {
181        option_double_equals(self.min, other.min) && option_double_equals(self.max, other.max)
182    }
183}
184
185impl HashComponent for DoubleBounds {
186    fn hash_component(&self, hasher: &mut ComponentHasher) {
187        if let (Some(min), Some(max)) = (self.min, self.max)
188            && java_double_equals(min, max)
189        {
190            hasher.put_double(min);
191            return;
192        }
193        let mut entries = Vec::new();
194        if let Some(min) = self.min {
195            push_hash_entry(&mut entries, "min", &min);
196        }
197        if let Some(max) = self.max {
198            push_hash_entry(&mut entries, "max", &max);
199        }
200        hash_entries(hasher, &mut entries);
201    }
202}
203
204const fn option_double_equals(left: Option<f64>, right: Option<f64>) -> bool {
205    match (left, right) {
206        (Some(left), Some(right)) => java_double_equals(left, right),
207        (None, None) => true,
208        _ => false,
209    }
210}
211
212const fn java_double_equals(left: f64, right: f64) -> bool {
213    java_double_bits(left) == java_double_bits(right)
214}
215
216fn java_double_compare(left: f64, right: f64) -> std::cmp::Ordering {
217    if left < right {
218        return std::cmp::Ordering::Less;
219    }
220    if left > right {
221        return std::cmp::Ordering::Greater;
222    }
223    java_double_bits(left).cmp(&java_double_bits(right))
224}
225
226const fn java_double_bits(value: f64) -> i64 {
227    if value.is_nan() {
228        return 0x7ff8_0000_0000_0000;
229    }
230    i64::from_ne_bytes(value.to_bits().to_ne_bytes())
231}
232
233/// One exact or ranged block-state property matcher.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum StatePropertyValueMatcher {
236    Exact(String),
237    Range {
238        min: Option<String>,
239        max: Option<String>,
240    },
241}
242
243impl StatePropertyValueMatcher {
244    fn matches(&self, property: &(impl Property + ?Sized), actual: &str) -> bool {
245        match self {
246            Self::Exact(expected) => {
247                property.compare_value_names(actual, expected) == Some(Ordering::Equal)
248            }
249            Self::Range { min, max } => {
250                if let Some(min) = min
251                    && property
252                        .compare_value_names(actual, min)
253                        .is_none_or(Ordering::is_lt)
254                {
255                    return false;
256                }
257                if let Some(max) = max
258                    && property
259                        .compare_value_names(actual, max)
260                        .is_none_or(Ordering::is_gt)
261                {
262                    return false;
263                }
264                true
265            }
266        }
267    }
268
269    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
270        if let Some(value) = tag.string() {
271            return Some(Self::Exact(value.to_string()));
272        }
273        let compound = tag.compound()?;
274        Some(Self::Range {
275            min: optional_string(compound, "min")?,
276            max: optional_string(compound, "max")?,
277        })
278    }
279
280    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
281        match self {
282            Self::Exact(value) => NbtTag::String(value.clone().into()),
283            Self::Range { min, max } => {
284                let mut compound = NbtCompound::new();
285                if let Some(min) = min {
286                    compound.insert("min", min.as_str());
287                }
288                if let Some(max) = max {
289                    compound.insert("max", max.as_str());
290                }
291                NbtTag::Compound(compound)
292            }
293        }
294    }
295
296    fn write_network(&self, writer: &mut impl Write) -> Result<()> {
297        match self {
298            Self::Exact(value) => {
299                true.write(writer)?;
300                write_utf(value, writer)
301            }
302            Self::Range { min, max } => {
303                false.write(writer)?;
304                write_optional_utf(min.as_deref(), writer)?;
305                write_optional_utf(max.as_deref(), writer)
306            }
307        }
308    }
309
310    fn read_network(data: &mut Cursor<&[u8]>) -> Result<Self> {
311        if bool::read(data)? {
312            Ok(Self::Exact(read_utf(data)?))
313        } else {
314            Ok(Self::Range {
315                min: read_optional_utf(data)?,
316                max: read_optional_utf(data)?,
317            })
318        }
319    }
320}
321
322impl HashComponent for StatePropertyValueMatcher {
323    fn hash_component(&self, hasher: &mut ComponentHasher) {
324        self.to_nbt_tag_ref().hash_component(hasher);
325    }
326}
327
328/// A named state property and its value matcher.
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct StatePropertyMatcher {
331    name: String,
332    value: StatePropertyValueMatcher,
333}
334
335impl StatePropertyMatcher {
336    #[must_use]
337    pub const fn new(name: String, value: StatePropertyValueMatcher) -> Self {
338        Self { name, value }
339    }
340
341    #[must_use]
342    pub fn name(&self) -> &str {
343        &self.name
344    }
345
346    #[must_use]
347    pub const fn value(&self) -> &StatePropertyValueMatcher {
348        &self.value
349    }
350}
351
352/// Vanilla state-properties predicate.
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct StatePropertiesPredicate {
355    properties: Vec<StatePropertyMatcher>,
356}
357
358impl StatePropertiesPredicate {
359    #[must_use]
360    pub fn new(properties: Vec<StatePropertyMatcher>) -> Option<Self> {
361        let mut names = rustc_hash::FxHashSet::default();
362        properties
363            .iter()
364            .all(|property| names.insert(property.name.clone()))
365            .then_some(Self { properties })
366    }
367
368    #[must_use]
369    pub fn properties(&self) -> &[StatePropertyMatcher] {
370        &self.properties
371    }
372
373    /// Tests this predicate against a registered block state.
374    #[must_use]
375    pub fn matches_block_state(&self, state: BlockStateId) -> bool {
376        let Some(block) = REGISTRY.blocks.by_state_id(state) else {
377            return false;
378        };
379        self.properties.iter().all(|matcher| {
380            let Some(index) = block
381                .properties
382                .iter()
383                .position(|property| property.get_name() == matcher.name())
384            else {
385                return false;
386            };
387            REGISTRY
388                .blocks
389                .get_property_str(state, matcher.name())
390                .is_some_and(|value| matcher.value().matches(block.properties[index], value))
391        })
392    }
393
394    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
395        let compound = tag.compound()?;
396        let mut properties = Vec::with_capacity(compound.len());
397        for (name, value) in compound.iter() {
398            properties.push(StatePropertyMatcher::new(
399                name.to_owned().try_into_string().ok()?,
400                StatePropertyValueMatcher::from_owned_nbt(value)?,
401            ));
402        }
403        Self::new(properties)
404    }
405
406    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
407        let mut compound = NbtCompound::new();
408        for property in &self.properties {
409            compound.insert(property.name.clone(), property.value.to_nbt_tag_ref());
410        }
411        NbtTag::Compound(compound)
412    }
413}
414
415impl WriteTo for StatePropertiesPredicate {
416    fn write(&self, writer: &mut impl Write) -> Result<()> {
417        write_len(self.properties.len(), writer)?;
418        for property in &self.properties {
419            write_utf(&property.name, writer)?;
420            property.value.write_network(writer)?;
421        }
422        Ok(())
423    }
424}
425
426impl ReadFrom for StatePropertiesPredicate {
427    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
428        let count = read_len(data, usize::MAX, "state property")?;
429        let mut properties = Vec::with_capacity(count.min(1024));
430        for _ in 0..count {
431            properties.push(StatePropertyMatcher::new(
432                read_utf(data)?,
433                StatePropertyValueMatcher::read_network(data)?,
434            ));
435        }
436        Self::new(properties).ok_or_else(|| Error::other("duplicate state property matcher"))
437    }
438}
439
440impl HashComponent for StatePropertiesPredicate {
441    fn hash_component(&self, hasher: &mut ComponentHasher) {
442        self.to_nbt_tag_ref().hash_component(hasher);
443    }
444}
445
446/// Vanilla partial-NBT predicate.
447#[derive(Debug, Clone)]
448pub struct NbtPredicate {
449    tag: NbtCompound,
450}
451
452impl NbtPredicate {
453    #[must_use]
454    pub fn new(tag: NbtCompound) -> Option<Self> {
455        normalize_nbt_compound(tag).map(|tag| Self { tag })
456    }
457
458    #[must_use]
459    pub const fn tag(&self) -> &NbtCompound {
460        &self.tag
461    }
462
463    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
464        match tag {
465            NbtTag::String(value) => {
466                let value = value.to_owned().try_into_string().ok()?;
467                Self::new(parse_snbt_compound(&value).ok()?)
468            }
469            NbtTag::Compound(compound) => Self::new(compound.clone()),
470            _ => None,
471        }
472    }
473
474    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
475        let Some(snbt) = to_canonical_snbt(&NbtTag::Compound(self.tag.clone())) else {
476            panic!("normalized NBT predicate became malformed");
477        };
478        NbtTag::String(snbt.into())
479    }
480}
481
482impl PartialEq for NbtPredicate {
483    fn eq(&self, other: &Self) -> bool {
484        steel_utils::nbt::nbt_compounds_equal(&self.tag, &other.tag)
485    }
486}
487
488impl WriteTo for NbtPredicate {
489    fn write(&self, writer: &mut impl Write) -> Result<()> {
490        let mut encoded = Vec::new();
491        NbtTag::Compound(self.tag.clone()).write(&mut encoded);
492        writer.write_all(&encoded)
493    }
494}
495
496impl ReadFrom for NbtPredicate {
497    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
498        let NbtTag::Compound(compound) = read_network_nbt(data)? else {
499            return Err(Error::other(
500                "NBT predicate network value is not a compound",
501            ));
502        };
503        Self::new(compound).ok_or_else(|| Error::other("NBT predicate contains malformed UTF-8"))
504    }
505}
506
507impl HashComponent for NbtPredicate {
508    fn hash_component(&self, hasher: &mut ComponentHasher) {
509        let NbtTag::String(value) = self.to_nbt_tag_ref() else {
510            unreachable!("NBT predicate codec always writes a string");
511        };
512        hasher.put_string(&value.to_string());
513    }
514}
515
516/// Vanilla block predicate used by adventure-mode item components.
517#[derive(Debug, Clone, PartialEq)]
518pub struct BlockPredicate {
519    blocks: Option<RegistryHolderSet<Block>>,
520    state: Option<StatePropertiesPredicate>,
521    nbt: Option<NbtPredicate>,
522    components: DataComponentMatchers,
523}
524
525impl BlockPredicate {
526    #[must_use]
527    pub const fn new(
528        blocks: Option<RegistryHolderSet<Block>>,
529        state: Option<StatePropertiesPredicate>,
530        nbt: Option<NbtPredicate>,
531        components: DataComponentMatchers,
532    ) -> Self {
533        Self {
534            blocks,
535            state,
536            nbt,
537            components,
538        }
539    }
540
541    #[must_use]
542    pub const fn blocks(&self) -> Option<&RegistryHolderSet<Block>> {
543        self.blocks.as_ref()
544    }
545
546    #[must_use]
547    pub const fn state(&self) -> Option<&StatePropertiesPredicate> {
548        self.state.as_ref()
549    }
550
551    #[must_use]
552    pub const fn nbt(&self) -> Option<&NbtPredicate> {
553        self.nbt.as_ref()
554    }
555
556    #[must_use]
557    pub const fn components(&self) -> &DataComponentMatchers {
558        &self.components
559    }
560
561    /// Tests the block and state-property portions of this predicate.
562    #[must_use]
563    pub fn matches_state(&self, state: BlockStateId) -> bool {
564        let Some(block) = REGISTRY.blocks.by_state_id(state) else {
565            return false;
566        };
567        if self
568            .blocks
569            .as_ref()
570            .is_some_and(|blocks| !blocks.contains(block))
571        {
572            return false;
573        }
574        self.state
575            .as_ref()
576            .is_none_or(|properties| properties.matches_block_state(state))
577    }
578
579    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
580        let compound = tag.compound()?;
581        Some(Self::new(
582            decode_optional(compound, "blocks", RegistryHolderSet::from_owned_nbt)?,
583            decode_optional(compound, "state", StatePropertiesPredicate::from_owned_nbt)?,
584            decode_optional(compound, "nbt", NbtPredicate::from_owned_nbt)?,
585            DataComponentMatchers::from_fields(compound)?,
586        ))
587    }
588
589    fn to_nbt_tag_ref(&self) -> NbtTag {
590        let mut compound = NbtCompound::new();
591        if let Some(blocks) = &self.blocks {
592            compound.insert("blocks", blocks.clone().to_nbt_tag());
593        }
594        if let Some(state) = &self.state {
595            compound.insert("state", state.to_nbt_tag_ref());
596        }
597        if let Some(nbt) = &self.nbt {
598            compound.insert("nbt", nbt.to_nbt_tag_ref());
599        }
600        self.components.write_fields(&mut compound);
601        NbtTag::Compound(compound)
602    }
603}
604
605impl WriteTo for BlockPredicate {
606    fn write(&self, writer: &mut impl Write) -> Result<()> {
607        self.blocks.write(writer)?;
608        self.state.write(writer)?;
609        self.nbt.write(writer)?;
610        self.components.write(writer)
611    }
612}
613
614impl ReadFrom for BlockPredicate {
615    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
616        Ok(Self::new(
617            Option::<RegistryHolderSet<Block>>::read(data)?,
618            Option::<StatePropertiesPredicate>::read(data)?,
619            Option::<NbtPredicate>::read(data)?,
620            DataComponentMatchers::read(data)?,
621        ))
622    }
623}
624
625impl HashComponent for BlockPredicate {
626    fn hash_component(&self, hasher: &mut ComponentHasher) {
627        let mut entries = Vec::new();
628        if let Some(blocks) = &self.blocks {
629            push_hash_entry(&mut entries, "blocks", blocks);
630        }
631        if let Some(state) = &self.state {
632            push_hash_entry(&mut entries, "state", state);
633        }
634        if let Some(nbt) = &self.nbt {
635            push_hash_entry(&mut entries, "nbt", nbt);
636        }
637        self.components.hash_fields(&mut entries);
638        hash_entries(hasher, &mut entries);
639    }
640}
641
642/// Non-empty list of block predicates used by `can_break` and `can_place_on`.
643#[derive(Debug, Clone, PartialEq)]
644pub struct AdventureModePredicate {
645    predicates: Vec<BlockPredicate>,
646}
647
648impl AdventureModePredicate {
649    #[must_use]
650    pub fn new(predicates: Vec<BlockPredicate>) -> Option<Self> {
651        (!predicates.is_empty()).then_some(Self { predicates })
652    }
653
654    #[must_use]
655    pub fn predicates(&self) -> &[BlockPredicate] {
656        &self.predicates
657    }
658}
659
660impl WriteTo for AdventureModePredicate {
661    fn write(&self, writer: &mut impl Write) -> Result<()> {
662        write_len(self.predicates.len(), writer)?;
663        for predicate in &self.predicates {
664            predicate.write(writer)?;
665        }
666        Ok(())
667    }
668}
669
670impl ReadFrom for AdventureModePredicate {
671    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
672        let count = read_len(data, usize::MAX, "adventure-mode predicate")?;
673        let mut predicates = Vec::with_capacity(count.min(1024));
674        for _ in 0..count {
675            predicates.push(BlockPredicate::read(data)?);
676        }
677        Self::new(predicates)
678            .ok_or_else(|| Error::other("adventure-mode predicate list cannot be empty"))
679    }
680}
681
682impl ToNbtTag for AdventureModePredicate {
683    fn to_nbt_tag(self) -> NbtTag {
684        if self.predicates.len() == 1 {
685            return self.predicates[0].to_nbt_tag_ref();
686        }
687        NbtTag::List(NbtList::Compound(
688            self.predicates
689                .iter()
690                .map(|predicate| {
691                    let NbtTag::Compound(compound) = predicate.to_nbt_tag_ref() else {
692                        unreachable!("block predicate codec always writes a compound");
693                    };
694                    compound
695                })
696                .collect(),
697        ))
698    }
699}
700
701impl FromNbtTag for AdventureModePredicate {
702    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
703        let tag = tag.to_owned();
704        let predicates = if tag.compound().is_some() {
705            vec![BlockPredicate::from_owned_nbt(&tag)?]
706        } else {
707            tag.list()?
708                .compounds()?
709                .iter()
710                .map(|compound| BlockPredicate::from_owned_nbt(&NbtTag::Compound(compound.clone())))
711                .collect::<Option<Vec<_>>>()?
712        };
713        Self::new(predicates)
714    }
715}
716
717impl HashComponent for AdventureModePredicate {
718    fn hash_component(&self, hasher: &mut ComponentHasher) {
719        if self.predicates.len() == 1 {
720            self.predicates[0].hash_component(hasher);
721            return;
722        }
723        hasher.start_list();
724        for predicate in &self.predicates {
725            hasher.put_component_hash(predicate);
726        }
727        hasher.end_list();
728    }
729}
730
731/// Vanilla item predicate used by lock codes and nested component predicates.
732#[derive(Debug, Clone, PartialEq)]
733pub struct ItemPredicate {
734    items: Option<RegistryHolderSet<Item>>,
735    count: IntBounds,
736    components: DataComponentMatchers,
737}
738
739impl ItemPredicate {
740    #[must_use]
741    pub const fn new(
742        items: Option<RegistryHolderSet<Item>>,
743        count: IntBounds,
744        components: DataComponentMatchers,
745    ) -> Self {
746        Self {
747            items,
748            count,
749            components,
750        }
751    }
752
753    #[must_use]
754    pub const fn any() -> Self {
755        Self::new(None, IntBounds::ANY, DataComponentMatchers::ANY)
756    }
757
758    #[must_use]
759    pub const fn items(&self) -> Option<&RegistryHolderSet<Item>> {
760        self.items.as_ref()
761    }
762
763    #[must_use]
764    pub const fn count(&self) -> IntBounds {
765        self.count
766    }
767
768    #[must_use]
769    pub const fn components(&self) -> &DataComponentMatchers {
770        &self.components
771    }
772
773    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
774        let compound = tag.compound()?;
775        Some(Self::new(
776            decode_optional(compound, "items", RegistryHolderSet::from_owned_nbt)?,
777            compound
778                .get("count")
779                .map_or(Some(IntBounds::ANY), IntBounds::from_owned_nbt)?,
780            DataComponentMatchers::from_fields(compound)?,
781        ))
782    }
783
784    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
785        let mut compound = NbtCompound::new();
786        if let Some(items) = &self.items {
787            compound.insert("items", items.clone().to_nbt_tag());
788        }
789        if !self.count.is_any() {
790            compound.insert("count", self.count.as_nbt_tag());
791        }
792        self.components.write_fields(&mut compound);
793        NbtTag::Compound(compound)
794    }
795}
796
797impl HashComponent for ItemPredicate {
798    fn hash_component(&self, hasher: &mut ComponentHasher) {
799        let mut entries = Vec::new();
800        if let Some(items) = &self.items {
801            push_hash_entry(&mut entries, "items", items);
802        }
803        if !self.count.is_any() {
804            push_hash_entry(&mut entries, "count", &self.count);
805        }
806        self.components.hash_fields(&mut entries);
807        hash_entries(hasher, &mut entries);
808    }
809}
810
811/// Vanilla lock-code component value.
812#[derive(Debug, Clone, PartialEq)]
813pub struct LockCode {
814    predicate: ItemPredicate,
815}
816
817impl LockCode {
818    pub const NO_LOCK: Self = Self {
819        predicate: ItemPredicate::any(),
820    };
821
822    #[must_use]
823    pub const fn new(predicate: ItemPredicate) -> Self {
824        Self { predicate }
825    }
826
827    #[must_use]
828    pub const fn predicate(&self) -> &ItemPredicate {
829        &self.predicate
830    }
831
832    /// Encodes this lock code as its Vanilla item-predicate NBT without consuming it.
833    #[must_use]
834    pub fn to_nbt_tag_ref(&self) -> NbtTag {
835        self.predicate.to_nbt_tag_ref()
836    }
837}
838
839impl WriteTo for LockCode {
840    fn write(&self, writer: &mut impl Write) -> Result<()> {
841        let mut encoded = Vec::new();
842        self.predicate.to_nbt_tag_ref().write(&mut encoded);
843        writer.write_all(&encoded)
844    }
845}
846
847impl ReadFrom for LockCode {
848    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
849        let tag = read_network_nbt(data)?;
850        ItemPredicate::from_owned_nbt(&tag)
851            .map(Self::new)
852            .ok_or_else(|| Error::other("invalid lock item predicate"))
853    }
854}
855
856impl ToNbtTag for LockCode {
857    fn to_nbt_tag(self) -> NbtTag {
858        self.predicate.to_nbt_tag_ref()
859    }
860}
861
862impl FromNbtTag for LockCode {
863    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
864        ItemPredicate::from_owned_nbt(&tag.to_owned()).map(Self::new)
865    }
866}
867
868impl HashComponent for LockCode {
869    fn hash_component(&self, hasher: &mut ComponentHasher) {
870        self.predicate.hash_component(hasher);
871    }
872}
873
874#[expect(
875    clippy::option_option,
876    reason = "the outer option rejects malformed present fields while the inner option represents absence"
877)]
878fn optional_string(compound: &NbtCompound, key: &str) -> Option<Option<String>> {
879    decode_optional(compound, key, |tag| {
880        tag.string()?.to_owned().try_into_string().ok()
881    })
882}
883
884#[expect(
885    clippy::option_option,
886    reason = "the outer option rejects malformed present fields while the inner option represents absence"
887)]
888pub(crate) fn decode_optional<T>(
889    compound: &NbtCompound,
890    key: &str,
891    decode: impl FnOnce(&NbtTag) -> Option<T>,
892) -> Option<Option<T>> {
893    match compound.get(key) {
894        Some(tag) => Some(Some(decode(tag)?)),
895        None => Some(None),
896    }
897}
898
899pub(crate) fn read_network_nbt(data: &mut Cursor<&[u8]>) -> Result<NbtTag> {
900    let tag = read_tag(data).map_err(|error| Error::other(format!("invalid NBT: {error:?}")))?;
901    let Some(heap_size) = vanilla_nbt_heap_size(&tag) else {
902        return Err(Error::other("NBT contains malformed modified UTF-8"));
903    };
904    if heap_size > DEFAULT_NBT_QUOTA {
905        return Err(Error::other(format!(
906            "NBT exceeds Vanilla's {DEFAULT_NBT_QUOTA}-byte heap quota"
907        )));
908    }
909    Ok(tag)
910}
911
912pub(crate) fn write_len(len: usize, writer: &mut impl Write) -> Result<()> {
913    let len = i32::try_from(len).map_err(|_| Error::other("list exceeds protocol range"))?;
914    VarInt(len).write(writer)
915}
916
917pub(crate) fn read_len(data: &mut Cursor<&[u8]>, max: usize, name: &str) -> Result<usize> {
918    let encoded = VarInt::read(data)?.0;
919    let len = usize::try_from(encoded)
920        .map_err(|_| Error::other(format!("negative {name} count: {encoded}")))?;
921    if len > max {
922        return Err(Error::other(format!("{name} count {len} exceeds {max}")));
923    }
924    Ok(len)
925}
926
927fn write_utf(value: &str, writer: &mut impl Write) -> Result<()> {
928    if value.encode_utf16().count() > MAX_UTF_LENGTH {
929        return Err(Error::other("string exceeds Vanilla's UTF-16 length limit"));
930    }
931    if value.len() > MAX_UTF_LENGTH * 3 {
932        return Err(Error::other("string exceeds Vanilla's UTF-8 length limit"));
933    }
934    write_len(value.len(), writer)?;
935    writer.write_all(value.as_bytes())
936}
937
938fn read_utf(data: &mut Cursor<&[u8]>) -> Result<String> {
939    use std::io::Read as _;
940
941    let len = read_len(data, MAX_UTF_LENGTH * 3, "string byte")?;
942    let mut bytes = vec![0; len];
943    data.read_exact(&mut bytes)?;
944    let value = String::from_utf8(bytes).map_err(Error::other)?;
945    if value.encode_utf16().count() > MAX_UTF_LENGTH {
946        return Err(Error::other("string exceeds Vanilla's UTF-16 length limit"));
947    }
948    Ok(value)
949}
950
951fn write_optional_utf(value: Option<&str>, writer: &mut impl Write) -> Result<()> {
952    value.is_some().write(writer)?;
953    if let Some(value) = value {
954        write_utf(value, writer)?;
955    }
956    Ok(())
957}
958
959fn read_optional_utf(data: &mut Cursor<&[u8]>) -> Result<Option<String>> {
960    bool::read(data)?.then(|| read_utf(data)).transpose()
961}
962
963pub(crate) fn push_hash_entry<T: HashComponent + ?Sized>(
964    entries: &mut Vec<HashEntry>,
965    key: &str,
966    value: &T,
967) {
968    let mut key_hasher = ComponentHasher::new();
969    key.hash_component(&mut key_hasher);
970    let mut value_hasher = ComponentHasher::new();
971    value.hash_component(&mut value_hasher);
972    entries.push(HashEntry::new(key_hasher, value_hasher));
973}
974
975pub(crate) fn push_prehashed_entry(
976    entries: &mut Vec<HashEntry>,
977    key: &str,
978    value_hasher: ComponentHasher,
979) {
980    let mut key_hasher = ComponentHasher::new();
981    key.hash_component(&mut key_hasher);
982    entries.push(HashEntry::new(key_hasher, value_hasher));
983}
984
985pub(crate) fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
986    sort_map_entries(entries);
987    hasher.start_map();
988    for entry in entries {
989        hasher.put_raw_bytes(&entry.key_bytes);
990        hasher.put_raw_bytes(&entry.value_bytes);
991    }
992    hasher.end_map();
993}
994
995#[cfg(test)]
996mod tests {
997    use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
998    use steel_utils::Direction;
999
1000    use super::{
1001        BlockPredicate, DoubleBounds, NbtPredicate, StatePropertiesPredicate, StatePropertyMatcher,
1002        StatePropertyValueMatcher,
1003    };
1004    use crate::blocks::block_state_ext::BlockStateExt as _;
1005    use crate::blocks::properties::BlockStateProperties;
1006    use crate::data_component_predicate::DataComponentMatchers;
1007    use crate::{RegistryHolderSet, init_vanilla_registry, vanilla_blocks};
1008
1009    #[test]
1010    fn double_bounds_use_java_ordering() {
1011        assert!(DoubleBounds::new(Some(-0.0), Some(0.0)).is_some());
1012        assert!(DoubleBounds::new(Some(0.0), Some(-0.0)).is_none());
1013        assert!(DoubleBounds::new(Some(f64::NAN), Some(f64::NAN)).is_some());
1014        assert!(DoubleBounds::new(Some(1.0), Some(f64::NAN)).is_some());
1015        assert!(DoubleBounds::new(Some(f64::NAN), Some(1.0)).is_none());
1016    }
1017
1018    #[test]
1019    fn nbt_predicate_persistence_round_trips_heterogeneous_lists() {
1020        let mut tag = NbtCompound::new();
1021        tag.insert(
1022            "values",
1023            NbtList::from(vec![NbtTag::Int(7), NbtTag::String("value".into())]),
1024        );
1025        let predicate = NbtPredicate::new(tag).expect("predicate NBT should normalize");
1026
1027        let encoded = predicate.to_nbt_tag_ref();
1028        assert_eq!(encoded, NbtTag::String("{values:[7,\"value\"]}".into()));
1029        assert_eq!(NbtPredicate::from_owned_nbt(&encoded), Some(predicate));
1030    }
1031
1032    #[test]
1033    fn block_predicates_use_typed_vanilla_property_order() {
1034        init_vanilla_registry();
1035
1036        let lit = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1037            "lit".to_owned(),
1038            StatePropertyValueMatcher::Range {
1039                min: Some("true".to_owned()),
1040                max: None,
1041            },
1042        )])
1043        .expect("one state property is valid");
1044        let ore = BlockPredicate::new(
1045            Some(RegistryHolderSet::Direct(vec![
1046                &vanilla_blocks::REDSTONE_ORE,
1047            ])),
1048            Some(lit),
1049            None,
1050            DataComponentMatchers::ANY,
1051        );
1052        let unlit_ore = vanilla_blocks::REDSTONE_ORE.default_state();
1053        assert!(!ore.matches_state(unlit_ore));
1054        assert!(ore.matches_state(unlit_ore.set_value(&BlockStateProperties::LIT, true)));
1055        assert!(!ore.matches_state(vanilla_blocks::REDSTONE_LAMP.default_state()));
1056
1057        let power = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1058            "power".to_owned(),
1059            StatePropertyValueMatcher::Range {
1060                min: Some("6".to_owned()),
1061                max: Some("8".to_owned()),
1062            },
1063        )])
1064        .expect("one state property is valid");
1065        let wire = vanilla_blocks::REDSTONE_WIRE.default_state();
1066        assert!(power.matches_block_state(wire.set_value(&BlockStateProperties::POWER, 7)));
1067        assert!(!power.matches_block_state(wire.set_value(&BlockStateProperties::POWER, 9)));
1068
1069        let facing = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1070            "facing".to_owned(),
1071            StatePropertyValueMatcher::Range {
1072                min: Some("up".to_owned()),
1073                max: None,
1074            },
1075        )])
1076        .expect("one state property is valid");
1077        let dispenser = vanilla_blocks::DISPENSER.default_state();
1078        assert!(facing.matches_block_state(
1079            dispenser.set_value(&BlockStateProperties::FACING, Direction::North)
1080        ));
1081        assert!(!facing.matches_block_state(
1082            dispenser.set_value(&BlockStateProperties::FACING, Direction::Down)
1083        ));
1084    }
1085}