steel_registry/data_components/components/
enchantments.rs1use rustc_hash::FxHashMap;
2use simdnbt::owned::{NbtCompound, NbtTag};
3use simdnbt::{FromNbtTag, ToNbtTag};
4use steel_utils::Identifier;
5use steel_utils::codec::VarInt;
6use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
7use steel_utils::nbt::NbtNumeric as _;
8use steel_utils::serial::{ReadFrom, WriteTo};
9
10use crate::{REGISTRY, RegistryExt};
11
12#[derive(Debug, Clone, PartialEq)]
19pub struct ItemEnchantments {
20 pub levels: FxHashMap<Identifier, u32>,
21}
22
23impl ItemEnchantments {
24 #[must_use]
25 pub fn empty() -> Self {
26 Self {
27 levels: FxHashMap::default(),
28 }
29 }
30
31 #[must_use]
32 pub fn get_level(&self, enchantment: &Identifier) -> u32 {
33 self.levels.get(enchantment).copied().unwrap_or(0)
34 }
35
36 pub fn set(&mut self, enchantment: Identifier, level: u32) {
37 if level == 0 {
38 self.levels.remove(&enchantment);
39 } else {
40 self.levels.insert(enchantment, level.min(255));
41 }
42 }
43
44 pub fn upgrade(&mut self, enchantment: Identifier, level: u32) {
46 if level > 0 {
47 let existing = self.get_level(&enchantment);
48 self.levels
49 .insert(enchantment, existing.max(level).min(255));
50 }
51 }
52
53 #[must_use]
54 pub fn is_empty(&self) -> bool {
55 self.levels.is_empty()
56 }
57
58 #[must_use]
59 pub fn len(&self) -> usize {
60 self.levels.len()
61 }
62
63 pub fn iter(&self) -> impl Iterator<Item = (&Identifier, &u32)> {
64 self.levels.iter()
65 }
66}
67
68impl IntoIterator for ItemEnchantments {
69 type Item = (Identifier, u32);
70
71 type IntoIter = <FxHashMap<Identifier, u32> as IntoIterator>::IntoIter;
72
73 fn into_iter(self) -> Self::IntoIter {
74 self.levels.into_iter()
75 }
76}
77
78impl Default for ItemEnchantments {
79 fn default() -> Self {
80 Self::empty()
81 }
82}
83
84impl WriteTo for ItemEnchantments {
86 fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
87 let count = i32::try_from(self.levels.len()).map_err(|_| {
88 std::io::Error::other(format!(
89 "Enchantment map too large: {} entries",
90 self.levels.len()
91 ))
92 })?;
93 VarInt(count).write(writer)?;
94 for (key, &level) in &self.levels {
95 let id = REGISTRY
96 .enchantments
97 .id_from_key(key)
98 .ok_or_else(|| std::io::Error::other(format!("Unknown enchantment: {key}")))?;
99 let id = i32::try_from(id).map_err(|_| {
100 std::io::Error::other(format!("Enchantment id out of protocol range: {id}"))
101 })?;
102 if level > 255 {
103 return Err(std::io::Error::other(format!(
104 "Enchantment {key} has invalid level {level}"
105 )));
106 }
107 VarInt(id).write(writer)?;
108 VarInt(level as i32).write(writer)?;
109 }
110 Ok(())
111 }
112}
113
114impl ReadFrom for ItemEnchantments {
115 fn read(data: &mut std::io::Cursor<&[u8]>) -> std::io::Result<Self> {
116 let count = VarInt::read(data)?.0;
117 let count = usize::try_from(count)
118 .map_err(|_| std::io::Error::other(format!("Negative enchantment count: {count}")))?;
119 let mut levels = FxHashMap::default();
120 levels.reserve(count.min(65_536));
121 for _ in 0..count {
122 let id = VarInt::read(data)?.0;
123 let id = usize::try_from(id)
124 .map_err(|_| std::io::Error::other(format!("Negative enchantment id: {id}")))?;
125 let level = VarInt::read(data)?.0;
126 if !(0..=255).contains(&level) {
127 return Err(std::io::Error::other(format!(
128 "Enchantment level out of range: {level}"
129 )));
130 }
131 let enchantment = REGISTRY
132 .enchantments
133 .by_id(id)
134 .ok_or_else(|| std::io::Error::other(format!("Unknown enchantment id: {id}")))?;
135 levels.insert(enchantment.key.clone(), level as u32);
136 }
137 Ok(Self { levels })
138 }
139}
140
141impl ToNbtTag for ItemEnchantments {
143 fn to_nbt_tag(self) -> NbtTag {
144 let mut compound = NbtCompound::new();
145 for (key, level) in &self.levels {
146 compound.insert(key.to_string(), NbtTag::Int(*level as i32));
147 }
148 NbtTag::Compound(compound)
149 }
150}
151
152impl FromNbtTag for ItemEnchantments {
153 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
154 let compound = tag.compound()?;
155 let mut levels = FxHashMap::default();
156 for (key, value) in compound.iter() {
157 let ident = key.to_str().parse::<Identifier>().ok()?;
158 REGISTRY.enchantments.by_key(&ident)?;
159 let level = value.codec_i32()?;
160 if !(1..=255).contains(&level) {
161 return None;
162 }
163 if levels.insert(ident, level as u32).is_some() {
164 return None;
165 }
166 }
167 Some(Self { levels })
168 }
169}
170
171impl HashComponent for ItemEnchantments {
172 fn hash_component(&self, hasher: &mut ComponentHasher) {
173 hasher.start_map();
174 let mut entries: Vec<_> = self
175 .levels
176 .iter()
177 .map(|(key, &level)| {
178 let mut key_hasher = ComponentHasher::new();
179 key_hasher.put_string(&key.to_string());
180 let mut value_hasher = ComponentHasher::new();
181 value_hasher.put_int(level as i32);
182 HashEntry::new(key_hasher, value_hasher)
183 })
184 .collect();
185 sort_map_entries(&mut entries);
186 for entry in &entries {
187 hasher.put_raw_bytes(&entry.key_bytes);
188 hasher.put_raw_bytes(&entry.value_bytes);
189 }
190 hasher.end_map();
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use std::io::Cursor;
197
198 use simdnbt::FromNbtTag;
199 use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
200 use simdnbt::owned::{NbtCompound, NbtTag};
201
202 use super::ItemEnchantments;
203 use crate::init_vanilla_registry;
204
205 fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
206 let mut bytes = Vec::new();
207 tag.write(&mut bytes);
208 let borrowed =
209 read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
210 visitor(borrowed.as_tag())
211 }
212
213 fn parse_enchantments(compound: NbtCompound) -> Option<ItemEnchantments> {
214 with_borrowed_tag(NbtTag::Compound(compound), ItemEnchantments::from_nbt_tag)
215 }
216
217 #[test]
218 fn enchantment_nbt_requires_known_keys_and_vanilla_levels() {
219 init_vanilla_registry();
220 let mut valid = NbtCompound::new();
221 valid.insert("minecraft:efficiency", 5_i8);
222 assert!(parse_enchantments(valid).is_some());
223
224 let mut unknown = NbtCompound::new();
225 unknown.insert("minecraft:not_an_enchantment", 1);
226 assert!(parse_enchantments(unknown).is_none());
227
228 let mut out_of_range = NbtCompound::new();
229 out_of_range.insert("minecraft:efficiency", 256);
230 assert!(parse_enchantments(out_of_range).is_none());
231 }
232}