Skip to main content

steel_registry/data_components/components/
block_state.rs

1//! Vanilla `minecraft:block_state` item component.
2
3use std::collections::BTreeMap;
4use std::io::{Cursor, Error, Result, Write};
5
6use simdnbt::owned::{NbtCompound, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::codec::VarInt;
9use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
10use steel_utils::serial::{PrefixedRead, PrefixedWrite, ReadFrom, WriteTo};
11
12/// String-valued block properties applied when a block item is placed.
13#[derive(Debug, Default, Clone, PartialEq, Eq)]
14pub struct BlockItemStateProperties {
15    properties: BTreeMap<String, String>,
16}
17
18impl BlockItemStateProperties {
19    #[must_use]
20    pub const fn empty() -> Self {
21        Self {
22            properties: BTreeMap::new(),
23        }
24    }
25
26    #[must_use]
27    pub const fn new(properties: BTreeMap<String, String>) -> Self {
28        Self { properties }
29    }
30
31    #[must_use]
32    pub const fn properties(&self) -> &BTreeMap<String, String> {
33        &self.properties
34    }
35
36    #[must_use]
37    pub fn get(&self, property: &str) -> Option<&str> {
38        self.properties.get(property).map(String::as_str)
39    }
40
41    #[must_use]
42    pub fn is_empty(&self) -> bool {
43        self.properties.is_empty()
44    }
45}
46
47impl WriteTo for BlockItemStateProperties {
48    fn write(&self, writer: &mut impl Write) -> Result<()> {
49        write_count(self.properties.len(), writer)?;
50        for (name, value) in &self.properties {
51            name.write_prefixed_bound::<VarInt>(writer, i16::MAX as usize)?;
52            value.write_prefixed_bound::<VarInt>(writer, i16::MAX as usize)?;
53        }
54        Ok(())
55    }
56}
57
58impl ReadFrom for BlockItemStateProperties {
59    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
60        let count = read_count(data)?;
61        let mut properties = BTreeMap::new();
62        for _ in 0..count {
63            let name = String::read_prefixed_bound::<VarInt>(data, i16::MAX as usize)?;
64            let value = String::read_prefixed_bound::<VarInt>(data, i16::MAX as usize)?;
65            properties.insert(name, value);
66        }
67        Ok(Self::new(properties))
68    }
69}
70
71impl ToNbtTag for BlockItemStateProperties {
72    fn to_nbt_tag(self) -> NbtTag {
73        let mut compound = NbtCompound::new();
74        for (name, value) in self.properties {
75            compound.insert(name, value);
76        }
77        NbtTag::Compound(compound)
78    }
79}
80
81impl FromNbtTag for BlockItemStateProperties {
82    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
83        let compound = tag.compound()?;
84        let properties = compound
85            .iter()
86            .map(|(name, value)| Some((name.to_string(), value.string()?.to_string())))
87            .collect::<Option<BTreeMap<_, _>>>()?;
88        Some(Self::new(properties))
89    }
90}
91
92impl HashComponent for BlockItemStateProperties {
93    fn hash_component(&self, hasher: &mut ComponentHasher) {
94        let mut entries = Vec::with_capacity(self.properties.len());
95        for (name, value) in &self.properties {
96            let mut key_hasher = ComponentHasher::new();
97            key_hasher.put_string(name);
98            let mut value_hasher = ComponentHasher::new();
99            value_hasher.put_string(value);
100            entries.push(HashEntry::new(key_hasher, value_hasher));
101        }
102        sort_map_entries(&mut entries);
103        hasher.start_map();
104        for entry in &entries {
105            hasher.put_raw_bytes(&entry.key_bytes);
106            hasher.put_raw_bytes(&entry.value_bytes);
107        }
108        hasher.end_map();
109    }
110}
111
112fn write_count(count: usize, writer: &mut impl Write) -> Result<()> {
113    let count = i32::try_from(count)
114        .map_err(|_| Error::other("Block-state property map exceeds protocol range"))?;
115    VarInt(count).write(writer)
116}
117
118fn read_count(data: &mut Cursor<&[u8]>) -> Result<usize> {
119    let count = VarInt::read(data)?.0;
120    usize::try_from(count).map_err(|_| Error::other(format!("Negative map size: {count}")))
121}
122
123#[cfg(test)]
124mod tests {
125    use std::collections::BTreeMap;
126    use std::io::Cursor;
127
128    use simdnbt::owned::{NbtCompound, NbtTag};
129    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
130    use steel_utils::hash::HashComponent as _;
131    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
132
133    use super::BlockItemStateProperties;
134    use crate::data_components::vanilla_components::BLOCK_STATE;
135    use crate::init_vanilla_registry;
136    use crate::{REGISTRY, RegistryExt};
137
138    fn parse(tag: NbtTag) -> Option<BlockItemStateProperties> {
139        let mut bytes = Vec::new();
140        tag.write(&mut bytes);
141        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
142        BlockItemStateProperties::from_nbt_tag(borrowed.as_tag())
143    }
144
145    #[test]
146    fn property_maps_round_trip_both_codecs_and_hash_as_maps() {
147        let value = BlockItemStateProperties::new(BTreeMap::from([
148            ("facing".to_owned(), "north".to_owned()),
149            ("level".to_owned(), "15".to_owned()),
150        ]));
151        let mut compound = NbtCompound::new();
152        compound.insert("facing", "north");
153        compound.insert("level", "15");
154        let nbt = NbtTag::Compound(compound);
155        assert_eq!(value.clone().to_nbt_tag(), nbt);
156        assert_eq!(parse(nbt.clone()), Some(value.clone()));
157        assert_eq!(value.compute_hash(), nbt.compute_hash());
158
159        let mut network = Vec::new();
160        value.write(&mut network).expect("properties should encode");
161        assert_eq!(
162            BlockItemStateProperties::read(&mut Cursor::new(network.as_slice()))
163                .expect("properties should decode"),
164            value
165        );
166    }
167
168    #[test]
169    fn empty_properties_use_an_empty_compound() {
170        let empty = BlockItemStateProperties::empty();
171        assert!(empty.is_empty());
172        assert_eq!(
173            empty.clone().to_nbt_tag(),
174            NbtTag::Compound(NbtCompound::new())
175        );
176    }
177
178    #[test]
179    fn extracted_block_items_keep_placement_properties() {
180        init_vanilla_registry();
181        let light = REGISTRY
182            .items
183            .by_key(&steel_utils::Identifier::vanilla_static("light"))
184            .expect("light should be registered");
185        assert_eq!(
186            light.components.get(BLOCK_STATE),
187            Some(BlockItemStateProperties::new(BTreeMap::from([(
188                "level".to_owned(),
189                "15".to_owned(),
190            )])))
191        );
192    }
193}