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: &dyn Property, 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        let values = REGISTRY.blocks.get_properties(state);
380
381        self.properties.iter().all(|matcher| {
382            let Some(index) = block
383                .properties
384                .iter()
385                .position(|property| property.get_name() == matcher.name())
386            else {
387                return false;
388            };
389            matcher
390                .value()
391                .matches(block.properties[index], values[index].1)
392        })
393    }
394
395    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
396        let compound = tag.compound()?;
397        let mut properties = Vec::with_capacity(compound.len());
398        for (name, value) in compound.iter() {
399            properties.push(StatePropertyMatcher::new(
400                name.to_owned().try_into_string().ok()?,
401                StatePropertyValueMatcher::from_owned_nbt(value)?,
402            ));
403        }
404        Self::new(properties)
405    }
406
407    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
408        let mut compound = NbtCompound::new();
409        for property in &self.properties {
410            compound.insert(property.name.clone(), property.value.to_nbt_tag_ref());
411        }
412        NbtTag::Compound(compound)
413    }
414}
415
416impl WriteTo for StatePropertiesPredicate {
417    fn write(&self, writer: &mut impl Write) -> Result<()> {
418        write_len(self.properties.len(), writer)?;
419        for property in &self.properties {
420            write_utf(&property.name, writer)?;
421            property.value.write_network(writer)?;
422        }
423        Ok(())
424    }
425}
426
427impl ReadFrom for StatePropertiesPredicate {
428    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
429        let count = read_len(data, usize::MAX, "state property")?;
430        let mut properties = Vec::with_capacity(count.min(1024));
431        for _ in 0..count {
432            properties.push(StatePropertyMatcher::new(
433                read_utf(data)?,
434                StatePropertyValueMatcher::read_network(data)?,
435            ));
436        }
437        Self::new(properties).ok_or_else(|| Error::other("duplicate state property matcher"))
438    }
439}
440
441impl HashComponent for StatePropertiesPredicate {
442    fn hash_component(&self, hasher: &mut ComponentHasher) {
443        self.to_nbt_tag_ref().hash_component(hasher);
444    }
445}
446
447/// Vanilla partial-NBT predicate.
448#[derive(Debug, Clone)]
449pub struct NbtPredicate {
450    tag: NbtCompound,
451}
452
453impl NbtPredicate {
454    #[must_use]
455    pub fn new(tag: NbtCompound) -> Option<Self> {
456        normalize_nbt_compound(tag).map(|tag| Self { tag })
457    }
458
459    #[must_use]
460    pub const fn tag(&self) -> &NbtCompound {
461        &self.tag
462    }
463
464    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
465        match tag {
466            NbtTag::String(value) => {
467                let value = value.to_owned().try_into_string().ok()?;
468                Self::new(parse_snbt_compound(&value).ok()?)
469            }
470            NbtTag::Compound(compound) => Self::new(compound.clone()),
471            _ => None,
472        }
473    }
474
475    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
476        let Some(snbt) = to_canonical_snbt(&NbtTag::Compound(self.tag.clone())) else {
477            panic!("normalized NBT predicate became malformed");
478        };
479        NbtTag::String(snbt.into())
480    }
481}
482
483impl PartialEq for NbtPredicate {
484    fn eq(&self, other: &Self) -> bool {
485        steel_utils::nbt::nbt_compounds_equal(&self.tag, &other.tag)
486    }
487}
488
489impl WriteTo for NbtPredicate {
490    fn write(&self, writer: &mut impl Write) -> Result<()> {
491        let mut encoded = Vec::new();
492        NbtTag::Compound(self.tag.clone()).write(&mut encoded);
493        writer.write_all(&encoded)
494    }
495}
496
497impl ReadFrom for NbtPredicate {
498    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
499        let NbtTag::Compound(compound) = read_network_nbt(data)? else {
500            return Err(Error::other(
501                "NBT predicate network value is not a compound",
502            ));
503        };
504        Self::new(compound).ok_or_else(|| Error::other("NBT predicate contains malformed UTF-8"))
505    }
506}
507
508impl HashComponent for NbtPredicate {
509    fn hash_component(&self, hasher: &mut ComponentHasher) {
510        let NbtTag::String(value) = self.to_nbt_tag_ref() else {
511            unreachable!("NBT predicate codec always writes a string");
512        };
513        hasher.put_string(&value.to_string());
514    }
515}
516
517/// Vanilla block predicate used by adventure-mode item components.
518#[derive(Debug, Clone, PartialEq)]
519pub struct BlockPredicate {
520    blocks: Option<RegistryHolderSet<Block>>,
521    state: Option<StatePropertiesPredicate>,
522    nbt: Option<NbtPredicate>,
523    components: DataComponentMatchers,
524}
525
526impl BlockPredicate {
527    #[must_use]
528    pub const fn new(
529        blocks: Option<RegistryHolderSet<Block>>,
530        state: Option<StatePropertiesPredicate>,
531        nbt: Option<NbtPredicate>,
532        components: DataComponentMatchers,
533    ) -> Self {
534        Self {
535            blocks,
536            state,
537            nbt,
538            components,
539        }
540    }
541
542    #[must_use]
543    pub const fn blocks(&self) -> Option<&RegistryHolderSet<Block>> {
544        self.blocks.as_ref()
545    }
546
547    #[must_use]
548    pub const fn state(&self) -> Option<&StatePropertiesPredicate> {
549        self.state.as_ref()
550    }
551
552    #[must_use]
553    pub const fn nbt(&self) -> Option<&NbtPredicate> {
554        self.nbt.as_ref()
555    }
556
557    #[must_use]
558    pub const fn components(&self) -> &DataComponentMatchers {
559        &self.components
560    }
561
562    /// Tests the block and state-property portions of this predicate.
563    #[must_use]
564    pub fn matches_state(&self, state: BlockStateId) -> bool {
565        let Some(block) = REGISTRY.blocks.by_state_id(state) else {
566            return false;
567        };
568        if self
569            .blocks
570            .as_ref()
571            .is_some_and(|blocks| !blocks.contains(block))
572        {
573            return false;
574        }
575        self.state
576            .as_ref()
577            .is_none_or(|properties| properties.matches_block_state(state))
578    }
579
580    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
581        let compound = tag.compound()?;
582        Some(Self::new(
583            decode_optional(compound, "blocks", RegistryHolderSet::from_owned_nbt)?,
584            decode_optional(compound, "state", StatePropertiesPredicate::from_owned_nbt)?,
585            decode_optional(compound, "nbt", NbtPredicate::from_owned_nbt)?,
586            DataComponentMatchers::from_fields(compound)?,
587        ))
588    }
589
590    fn to_nbt_tag_ref(&self) -> NbtTag {
591        let mut compound = NbtCompound::new();
592        if let Some(blocks) = &self.blocks {
593            compound.insert("blocks", blocks.clone().to_nbt_tag());
594        }
595        if let Some(state) = &self.state {
596            compound.insert("state", state.to_nbt_tag_ref());
597        }
598        if let Some(nbt) = &self.nbt {
599            compound.insert("nbt", nbt.to_nbt_tag_ref());
600        }
601        self.components.write_fields(&mut compound);
602        NbtTag::Compound(compound)
603    }
604}
605
606impl WriteTo for BlockPredicate {
607    fn write(&self, writer: &mut impl Write) -> Result<()> {
608        self.blocks.write(writer)?;
609        self.state.write(writer)?;
610        self.nbt.write(writer)?;
611        self.components.write(writer)
612    }
613}
614
615impl ReadFrom for BlockPredicate {
616    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
617        Ok(Self::new(
618            Option::<RegistryHolderSet<Block>>::read(data)?,
619            Option::<StatePropertiesPredicate>::read(data)?,
620            Option::<NbtPredicate>::read(data)?,
621            DataComponentMatchers::read(data)?,
622        ))
623    }
624}
625
626impl HashComponent for BlockPredicate {
627    fn hash_component(&self, hasher: &mut ComponentHasher) {
628        let mut entries = Vec::new();
629        if let Some(blocks) = &self.blocks {
630            push_hash_entry(&mut entries, "blocks", blocks);
631        }
632        if let Some(state) = &self.state {
633            push_hash_entry(&mut entries, "state", state);
634        }
635        if let Some(nbt) = &self.nbt {
636            push_hash_entry(&mut entries, "nbt", nbt);
637        }
638        self.components.hash_fields(&mut entries);
639        hash_entries(hasher, &mut entries);
640    }
641}
642
643/// Non-empty list of block predicates used by `can_break` and `can_place_on`.
644#[derive(Debug, Clone, PartialEq)]
645pub struct AdventureModePredicate {
646    predicates: Vec<BlockPredicate>,
647}
648
649impl AdventureModePredicate {
650    #[must_use]
651    pub fn new(predicates: Vec<BlockPredicate>) -> Option<Self> {
652        (!predicates.is_empty()).then_some(Self { predicates })
653    }
654
655    #[must_use]
656    pub fn predicates(&self) -> &[BlockPredicate] {
657        &self.predicates
658    }
659}
660
661impl WriteTo for AdventureModePredicate {
662    fn write(&self, writer: &mut impl Write) -> Result<()> {
663        write_len(self.predicates.len(), writer)?;
664        for predicate in &self.predicates {
665            predicate.write(writer)?;
666        }
667        Ok(())
668    }
669}
670
671impl ReadFrom for AdventureModePredicate {
672    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
673        let count = read_len(data, usize::MAX, "adventure-mode predicate")?;
674        let mut predicates = Vec::with_capacity(count.min(1024));
675        for _ in 0..count {
676            predicates.push(BlockPredicate::read(data)?);
677        }
678        Self::new(predicates)
679            .ok_or_else(|| Error::other("adventure-mode predicate list cannot be empty"))
680    }
681}
682
683impl ToNbtTag for AdventureModePredicate {
684    fn to_nbt_tag(self) -> NbtTag {
685        if self.predicates.len() == 1 {
686            return self.predicates[0].to_nbt_tag_ref();
687        }
688        NbtTag::List(NbtList::Compound(
689            self.predicates
690                .iter()
691                .map(|predicate| {
692                    let NbtTag::Compound(compound) = predicate.to_nbt_tag_ref() else {
693                        unreachable!("block predicate codec always writes a compound");
694                    };
695                    compound
696                })
697                .collect(),
698        ))
699    }
700}
701
702impl FromNbtTag for AdventureModePredicate {
703    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
704        let tag = tag.to_owned();
705        let predicates = if tag.compound().is_some() {
706            vec![BlockPredicate::from_owned_nbt(&tag)?]
707        } else {
708            tag.list()?
709                .compounds()?
710                .iter()
711                .map(|compound| BlockPredicate::from_owned_nbt(&NbtTag::Compound(compound.clone())))
712                .collect::<Option<Vec<_>>>()?
713        };
714        Self::new(predicates)
715    }
716}
717
718impl HashComponent for AdventureModePredicate {
719    fn hash_component(&self, hasher: &mut ComponentHasher) {
720        if self.predicates.len() == 1 {
721            self.predicates[0].hash_component(hasher);
722            return;
723        }
724        hasher.start_list();
725        for predicate in &self.predicates {
726            hasher.put_component_hash(predicate);
727        }
728        hasher.end_list();
729    }
730}
731
732/// Vanilla item predicate used by lock codes and nested component predicates.
733#[derive(Debug, Clone, PartialEq)]
734pub struct ItemPredicate {
735    items: Option<RegistryHolderSet<Item>>,
736    count: IntBounds,
737    components: DataComponentMatchers,
738}
739
740impl ItemPredicate {
741    #[must_use]
742    pub const fn new(
743        items: Option<RegistryHolderSet<Item>>,
744        count: IntBounds,
745        components: DataComponentMatchers,
746    ) -> Self {
747        Self {
748            items,
749            count,
750            components,
751        }
752    }
753
754    #[must_use]
755    pub const fn any() -> Self {
756        Self::new(None, IntBounds::ANY, DataComponentMatchers::ANY)
757    }
758
759    #[must_use]
760    pub const fn items(&self) -> Option<&RegistryHolderSet<Item>> {
761        self.items.as_ref()
762    }
763
764    #[must_use]
765    pub const fn count(&self) -> IntBounds {
766        self.count
767    }
768
769    #[must_use]
770    pub const fn components(&self) -> &DataComponentMatchers {
771        &self.components
772    }
773
774    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
775        let compound = tag.compound()?;
776        Some(Self::new(
777            decode_optional(compound, "items", RegistryHolderSet::from_owned_nbt)?,
778            compound
779                .get("count")
780                .map_or(Some(IntBounds::ANY), IntBounds::from_owned_nbt)?,
781            DataComponentMatchers::from_fields(compound)?,
782        ))
783    }
784
785    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
786        let mut compound = NbtCompound::new();
787        if let Some(items) = &self.items {
788            compound.insert("items", items.clone().to_nbt_tag());
789        }
790        if !self.count.is_any() {
791            compound.insert("count", self.count.as_nbt_tag());
792        }
793        self.components.write_fields(&mut compound);
794        NbtTag::Compound(compound)
795    }
796}
797
798impl HashComponent for ItemPredicate {
799    fn hash_component(&self, hasher: &mut ComponentHasher) {
800        let mut entries = Vec::new();
801        if let Some(items) = &self.items {
802            push_hash_entry(&mut entries, "items", items);
803        }
804        if !self.count.is_any() {
805            push_hash_entry(&mut entries, "count", &self.count);
806        }
807        self.components.hash_fields(&mut entries);
808        hash_entries(hasher, &mut entries);
809    }
810}
811
812/// Vanilla lock-code component value.
813#[derive(Debug, Clone, PartialEq)]
814pub struct LockCode {
815    predicate: ItemPredicate,
816}
817
818impl LockCode {
819    pub const NO_LOCK: Self = Self {
820        predicate: ItemPredicate::any(),
821    };
822
823    #[must_use]
824    pub const fn new(predicate: ItemPredicate) -> Self {
825        Self { predicate }
826    }
827
828    #[must_use]
829    pub const fn predicate(&self) -> &ItemPredicate {
830        &self.predicate
831    }
832
833    /// Encodes this lock code as its Vanilla item-predicate NBT without consuming it.
834    #[must_use]
835    pub fn to_nbt_tag_ref(&self) -> NbtTag {
836        self.predicate.to_nbt_tag_ref()
837    }
838}
839
840impl WriteTo for LockCode {
841    fn write(&self, writer: &mut impl Write) -> Result<()> {
842        let mut encoded = Vec::new();
843        self.predicate.to_nbt_tag_ref().write(&mut encoded);
844        writer.write_all(&encoded)
845    }
846}
847
848impl ReadFrom for LockCode {
849    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
850        let tag = read_network_nbt(data)?;
851        ItemPredicate::from_owned_nbt(&tag)
852            .map(Self::new)
853            .ok_or_else(|| Error::other("invalid lock item predicate"))
854    }
855}
856
857impl ToNbtTag for LockCode {
858    fn to_nbt_tag(self) -> NbtTag {
859        self.predicate.to_nbt_tag_ref()
860    }
861}
862
863impl FromNbtTag for LockCode {
864    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
865        ItemPredicate::from_owned_nbt(&tag.to_owned()).map(Self::new)
866    }
867}
868
869impl HashComponent for LockCode {
870    fn hash_component(&self, hasher: &mut ComponentHasher) {
871        self.predicate.hash_component(hasher);
872    }
873}
874
875#[expect(
876    clippy::option_option,
877    reason = "the outer option rejects malformed present fields while the inner option represents absence"
878)]
879fn optional_string(compound: &NbtCompound, key: &str) -> Option<Option<String>> {
880    decode_optional(compound, key, |tag| {
881        tag.string()?.to_owned().try_into_string().ok()
882    })
883}
884
885#[expect(
886    clippy::option_option,
887    reason = "the outer option rejects malformed present fields while the inner option represents absence"
888)]
889pub(crate) fn decode_optional<T>(
890    compound: &NbtCompound,
891    key: &str,
892    decode: impl FnOnce(&NbtTag) -> Option<T>,
893) -> Option<Option<T>> {
894    match compound.get(key) {
895        Some(tag) => Some(Some(decode(tag)?)),
896        None => Some(None),
897    }
898}
899
900pub(crate) fn read_network_nbt(data: &mut Cursor<&[u8]>) -> Result<NbtTag> {
901    let tag = read_tag(data).map_err(|error| Error::other(format!("invalid NBT: {error:?}")))?;
902    let Some(heap_size) = vanilla_nbt_heap_size(&tag) else {
903        return Err(Error::other("NBT contains malformed modified UTF-8"));
904    };
905    if heap_size > DEFAULT_NBT_QUOTA {
906        return Err(Error::other(format!(
907            "NBT exceeds Vanilla's {DEFAULT_NBT_QUOTA}-byte heap quota"
908        )));
909    }
910    Ok(tag)
911}
912
913pub(crate) fn write_len(len: usize, writer: &mut impl Write) -> Result<()> {
914    let len = i32::try_from(len).map_err(|_| Error::other("list exceeds protocol range"))?;
915    VarInt(len).write(writer)
916}
917
918pub(crate) fn read_len(data: &mut Cursor<&[u8]>, max: usize, name: &str) -> Result<usize> {
919    let encoded = VarInt::read(data)?.0;
920    let len = usize::try_from(encoded)
921        .map_err(|_| Error::other(format!("negative {name} count: {encoded}")))?;
922    if len > max {
923        return Err(Error::other(format!("{name} count {len} exceeds {max}")));
924    }
925    Ok(len)
926}
927
928fn write_utf(value: &str, writer: &mut impl Write) -> Result<()> {
929    if value.encode_utf16().count() > MAX_UTF_LENGTH {
930        return Err(Error::other("string exceeds Vanilla's UTF-16 length limit"));
931    }
932    if value.len() > MAX_UTF_LENGTH * 3 {
933        return Err(Error::other("string exceeds Vanilla's UTF-8 length limit"));
934    }
935    write_len(value.len(), writer)?;
936    writer.write_all(value.as_bytes())
937}
938
939fn read_utf(data: &mut Cursor<&[u8]>) -> Result<String> {
940    use std::io::Read as _;
941
942    let len = read_len(data, MAX_UTF_LENGTH * 3, "string byte")?;
943    let mut bytes = vec![0; len];
944    data.read_exact(&mut bytes)?;
945    let value = String::from_utf8(bytes).map_err(Error::other)?;
946    if value.encode_utf16().count() > MAX_UTF_LENGTH {
947        return Err(Error::other("string exceeds Vanilla's UTF-16 length limit"));
948    }
949    Ok(value)
950}
951
952fn write_optional_utf(value: Option<&str>, writer: &mut impl Write) -> Result<()> {
953    value.is_some().write(writer)?;
954    if let Some(value) = value {
955        write_utf(value, writer)?;
956    }
957    Ok(())
958}
959
960fn read_optional_utf(data: &mut Cursor<&[u8]>) -> Result<Option<String>> {
961    bool::read(data)?.then(|| read_utf(data)).transpose()
962}
963
964pub(crate) fn push_hash_entry<T: HashComponent + ?Sized>(
965    entries: &mut Vec<HashEntry>,
966    key: &str,
967    value: &T,
968) {
969    let mut key_hasher = ComponentHasher::new();
970    key.hash_component(&mut key_hasher);
971    let mut value_hasher = ComponentHasher::new();
972    value.hash_component(&mut value_hasher);
973    entries.push(HashEntry::new(key_hasher, value_hasher));
974}
975
976pub(crate) fn push_prehashed_entry(
977    entries: &mut Vec<HashEntry>,
978    key: &str,
979    value_hasher: ComponentHasher,
980) {
981    let mut key_hasher = ComponentHasher::new();
982    key.hash_component(&mut key_hasher);
983    entries.push(HashEntry::new(key_hasher, value_hasher));
984}
985
986pub(crate) fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
987    sort_map_entries(entries);
988    hasher.start_map();
989    for entry in entries {
990        hasher.put_raw_bytes(&entry.key_bytes);
991        hasher.put_raw_bytes(&entry.value_bytes);
992    }
993    hasher.end_map();
994}
995
996#[cfg(test)]
997mod tests {
998    use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
999    use steel_utils::Direction;
1000
1001    use super::{
1002        BlockPredicate, DoubleBounds, NbtPredicate, StatePropertiesPredicate, StatePropertyMatcher,
1003        StatePropertyValueMatcher,
1004    };
1005    use crate::blocks::block_state_ext::BlockStateExt as _;
1006    use crate::blocks::properties::BlockStateProperties;
1007    use crate::data_component_predicate::DataComponentMatchers;
1008    use crate::{RegistryHolderSet, init_vanilla_registry, vanilla_blocks};
1009
1010    #[test]
1011    fn double_bounds_use_java_ordering() {
1012        assert!(DoubleBounds::new(Some(-0.0), Some(0.0)).is_some());
1013        assert!(DoubleBounds::new(Some(0.0), Some(-0.0)).is_none());
1014        assert!(DoubleBounds::new(Some(f64::NAN), Some(f64::NAN)).is_some());
1015        assert!(DoubleBounds::new(Some(1.0), Some(f64::NAN)).is_some());
1016        assert!(DoubleBounds::new(Some(f64::NAN), Some(1.0)).is_none());
1017    }
1018
1019    #[test]
1020    fn nbt_predicate_persistence_round_trips_heterogeneous_lists() {
1021        let mut tag = NbtCompound::new();
1022        tag.insert(
1023            "values",
1024            NbtList::from(vec![NbtTag::Int(7), NbtTag::String("value".into())]),
1025        );
1026        let predicate = NbtPredicate::new(tag).expect("predicate NBT should normalize");
1027
1028        let encoded = predicate.to_nbt_tag_ref();
1029        assert_eq!(encoded, NbtTag::String("{values:[7,\"value\"]}".into()));
1030        assert_eq!(NbtPredicate::from_owned_nbt(&encoded), Some(predicate));
1031    }
1032
1033    #[test]
1034    fn block_predicates_use_typed_vanilla_property_order() {
1035        init_vanilla_registry();
1036
1037        let lit = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1038            "lit".to_owned(),
1039            StatePropertyValueMatcher::Range {
1040                min: Some("true".to_owned()),
1041                max: None,
1042            },
1043        )])
1044        .expect("one state property is valid");
1045        let ore = BlockPredicate::new(
1046            Some(RegistryHolderSet::Direct(vec![
1047                &vanilla_blocks::REDSTONE_ORE,
1048            ])),
1049            Some(lit),
1050            None,
1051            DataComponentMatchers::ANY,
1052        );
1053        let unlit_ore = vanilla_blocks::REDSTONE_ORE.default_state();
1054        assert!(!ore.matches_state(unlit_ore));
1055        assert!(ore.matches_state(unlit_ore.set_value(&BlockStateProperties::LIT, true)));
1056        assert!(!ore.matches_state(vanilla_blocks::REDSTONE_LAMP.default_state()));
1057
1058        let power = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1059            "power".to_owned(),
1060            StatePropertyValueMatcher::Range {
1061                min: Some("6".to_owned()),
1062                max: Some("8".to_owned()),
1063            },
1064        )])
1065        .expect("one state property is valid");
1066        let wire = vanilla_blocks::REDSTONE_WIRE.default_state();
1067        assert!(power.matches_block_state(wire.set_value(&BlockStateProperties::POWER, 7)));
1068        assert!(!power.matches_block_state(wire.set_value(&BlockStateProperties::POWER, 9)));
1069
1070        let facing = StatePropertiesPredicate::new(vec![StatePropertyMatcher::new(
1071            "facing".to_owned(),
1072            StatePropertyValueMatcher::Range {
1073                min: Some("up".to_owned()),
1074                max: None,
1075            },
1076        )])
1077        .expect("one state property is valid");
1078        let dispenser = vanilla_blocks::DISPENSER.default_state();
1079        assert!(facing.matches_block_state(
1080            dispenser.set_value(&BlockStateProperties::FACING, Direction::North)
1081        ));
1082        assert!(!facing.matches_block_state(
1083            dispenser.set_value(&BlockStateProperties::FACING, Direction::Down)
1084        ));
1085    }
1086}