steel_registry/data_components/components/
block_state.rs1use 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#[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}