steel_registry/data_components/components/
attribute_modifiers.rs1#![cfg_attr(
3 test,
4 expect(
5 clippy::unwrap_used,
6 reason = "attribute modifier tests build in-memory byte buffers with infallible writes"
7 )
8)]
9
10use std::io::{Cursor, Result, Write};
11
12use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
13use simdnbt::{FromNbtTag, ToNbtTag};
14use steel_utils::Identifier;
15use steel_utils::codec::VarInt;
16use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
17use steel_utils::nbt::NbtNumeric as _;
18use steel_utils::serial::{ReadFrom, WriteTo};
19use text_components::TextComponent;
20
21use crate::attribute::{AttributeModifierOperation, AttributeRef};
22use crate::equipment::{EquipmentSlot, EquipmentSlotGroup};
23use crate::{REGISTRY, RegistryEntry, RegistryExt};
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct ItemAttributeModifiers {
28 pub modifiers: Vec<ItemAttributeModifierEntry>,
29}
30
31impl ItemAttributeModifiers {
32 #[must_use]
33 pub const fn empty() -> Self {
34 Self {
35 modifiers: Vec::new(),
36 }
37 }
38
39 #[must_use]
40 pub const fn is_empty(&self) -> bool {
41 self.modifiers.is_empty()
42 }
43
44 pub fn for_slot(
45 &self,
46 slot: EquipmentSlot,
47 ) -> impl Iterator<Item = &ItemAttributeModifierEntry> {
48 self.modifiers
49 .iter()
50 .filter(move |entry| entry.slot.test(slot))
51 }
52
53 #[must_use]
58 pub fn compute(&self, attribute: AttributeRef, base_value: f64, slot: EquipmentSlot) -> f64 {
59 let mut value = base_value;
60 for entry in &self.modifiers {
61 if entry.slot.test(slot) && entry.attribute.key == attribute.key {
62 value += match entry.operation {
63 AttributeModifierOperation::AddValue => entry.amount,
64 AttributeModifierOperation::AddMultipliedBase => entry.amount * base_value,
65 AttributeModifierOperation::AddMultipliedTotal => entry.amount * value,
66 };
67 }
68 }
69 value
70 }
71}
72
73impl Default for ItemAttributeModifiers {
74 fn default() -> Self {
75 Self::empty()
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct ItemAttributeModifierEntry {
82 pub attribute: AttributeRef,
83 pub id: Identifier,
84 pub amount: f64,
85 pub operation: AttributeModifierOperation,
86 pub slot: EquipmentSlotGroup,
87 pub display: ItemAttributeModifierDisplay,
88}
89
90impl PartialEq for ItemAttributeModifierEntry {
91 fn eq(&self, other: &Self) -> bool {
92 self.attribute.key == other.attribute.key
93 && self.id == other.id
94 && self.amount == other.amount
95 && self.operation == other.operation
96 && self.slot == other.slot
97 && self.display == other.display
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
103pub enum ItemAttributeModifierDisplay {
104 Default,
105 Hidden,
106 OverrideText(Box<TextComponent>),
107}
108
109impl ItemAttributeModifierDisplay {
110 #[must_use]
111 pub const fn id(&self) -> i32 {
112 match self {
113 Self::Default => 0,
114 Self::Hidden => 1,
115 Self::OverrideText(_) => 2,
116 }
117 }
118
119 #[must_use]
120 pub const fn name(&self) -> &'static str {
121 match self {
122 Self::Default => "default",
123 Self::Hidden => "hidden",
124 Self::OverrideText(_) => "override",
125 }
126 }
127
128 fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> {
129 let display_type = compound.get("type")?.string()?.to_str();
130 match display_type.as_ref() {
131 "default" => Some(Self::Default),
132 "hidden" => Some(Self::Hidden),
133 "override" => {
134 let value = compound.get("value")?;
135 Some(Self::OverrideText(Box::new(TextComponent::from_nbt_tag(
136 value,
137 )?)))
138 }
139 _ => None,
140 }
141 }
142
143 fn to_nbt_tag_ref(&self) -> NbtTag {
144 let mut compound = NbtCompound::new();
145 compound.insert("type", self.name());
146 if let Self::OverrideText(text) = self {
147 compound.insert("value", text.to_codec_nbt());
148 }
149 NbtTag::Compound(compound)
150 }
151}
152
153impl WriteTo for ItemAttributeModifiers {
154 fn write(&self, writer: &mut impl Write) -> Result<()> {
155 let count = i32::try_from(self.modifiers.len()).map_err(|_| {
156 std::io::Error::other(format!(
157 "Attribute modifier list too large: {} entries",
158 self.modifiers.len()
159 ))
160 })?;
161 VarInt(count).write(writer)?;
162 for entry in &self.modifiers {
163 entry.write(writer)?;
164 }
165 Ok(())
166 }
167}
168
169impl ReadFrom for ItemAttributeModifiers {
170 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
171 let count = VarInt::read(data)?.0;
172 let count = usize::try_from(count).map_err(|_| {
173 std::io::Error::other(format!("Negative attribute modifier count: {count}"))
174 })?;
175 let mut modifiers = Vec::with_capacity(count.min(65_536));
176 for _ in 0..count {
177 modifiers.push(ItemAttributeModifierEntry::read(data)?);
178 }
179 Ok(Self { modifiers })
180 }
181}
182
183impl WriteTo for ItemAttributeModifierEntry {
184 fn write(&self, writer: &mut impl Write) -> Result<()> {
185 let attribute_id = self.attribute.try_id().ok_or_else(|| {
186 std::io::Error::other(format!("Unknown attribute: {}", self.attribute.key))
187 })?;
188 let attribute_id = i32::try_from(attribute_id).map_err(|_| {
189 std::io::Error::other(format!(
190 "Attribute id out of protocol range: {attribute_id}"
191 ))
192 })?;
193 VarInt(attribute_id).write(writer)?;
194 self.id.write(writer)?;
195 self.amount.write(writer)?;
196 self.operation.write(writer)?;
197 VarInt(self.slot.id()).write(writer)?;
198 self.display.write(writer)
199 }
200}
201
202impl ReadFrom for ItemAttributeModifierEntry {
203 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
204 let attribute_id = VarInt::read(data)?.0;
205 let attribute_id = usize::try_from(attribute_id)
206 .map_err(|_| std::io::Error::other(format!("Negative attribute id: {attribute_id}")))?;
207 let attribute = REGISTRY.attributes.by_id(attribute_id).ok_or_else(|| {
208 std::io::Error::other(format!("Unknown attribute id: {attribute_id}"))
209 })?;
210 let id = Identifier::read(data)?;
211 let amount = f64::read(data)?;
212 let operation = AttributeModifierOperation::read(data)?;
213 let slot_id = VarInt::read(data)?.0;
214 let slot = EquipmentSlotGroup::by_id(slot_id);
215 let display = ItemAttributeModifierDisplay::read(data)?;
216
217 Ok(Self {
218 attribute,
219 id,
220 amount,
221 operation,
222 slot,
223 display,
224 })
225 }
226}
227
228impl WriteTo for ItemAttributeModifierDisplay {
229 fn write(&self, writer: &mut impl Write) -> Result<()> {
230 VarInt(self.id()).write(writer)?;
231 if let Self::OverrideText(text) = self {
232 text.write(writer)?;
233 }
234 Ok(())
235 }
236}
237
238impl ReadFrom for ItemAttributeModifierDisplay {
239 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
240 let display_id = VarInt::read(data)?.0;
241 match display_id {
242 1 => Ok(Self::Hidden),
243 2 => Ok(Self::OverrideText(Box::new(TextComponent::read(data)?))),
244 _ => Ok(Self::Default),
245 }
246 }
247}
248
249impl ToNbtTag for ItemAttributeModifiers {
250 fn to_nbt_tag(self) -> NbtTag {
251 NbtTag::List(NbtList::Compound(
252 self.modifiers
253 .into_iter()
254 .map(ItemAttributeModifierEntry::into_nbt_compound)
255 .collect(),
256 ))
257 }
258}
259
260impl ItemAttributeModifierEntry {
261 fn into_nbt_compound(self) -> NbtCompound {
262 let mut compound = NbtCompound::new();
263 compound.insert("type", self.attribute.key.to_string());
264 compound.insert("id", self.id.to_string());
265 compound.insert("amount", self.amount);
266 compound.insert("operation", self.operation.name());
267 if self.slot != EquipmentSlotGroup::Any {
268 compound.insert("slot", self.slot.name());
269 }
270 if !matches!(self.display, ItemAttributeModifierDisplay::Default) {
271 compound.insert("display", self.display.to_nbt_tag_ref());
272 }
273 compound
274 }
275}
276
277impl FromNbtTag for ItemAttributeModifiers {
278 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
279 let entries = tag.list()?.compounds()?;
280 let mut modifiers = Vec::with_capacity(entries.len());
281
282 for compound in entries {
283 let attribute_key = compound
284 .get("type")
285 .and_then(|tag| tag.string())
286 .and_then(|value| value.to_str().parse::<Identifier>().ok())?;
287 let attribute = REGISTRY.attributes.by_key(&attribute_key)?;
288 let id = compound
289 .get("id")
290 .and_then(|tag| tag.string())
291 .and_then(|value| value.to_str().parse::<Identifier>().ok())?;
292 let amount = compound.get("amount")?.codec_f64()?;
293 let operation = compound
294 .get("operation")
295 .and_then(|tag| tag.string())
296 .and_then(|value| AttributeModifierOperation::by_name(value.to_str().as_ref()))?;
297 let slot = match compound.get("slot") {
298 Some(tag) => EquipmentSlotGroup::by_name(tag.string()?.to_str().as_ref())?,
299 None => EquipmentSlotGroup::Any,
300 };
301 let display = match compound.get("display") {
302 Some(tag) => ItemAttributeModifierDisplay::from_nbt_compound(tag.compound()?)?,
303 None => ItemAttributeModifierDisplay::Default,
304 };
305
306 modifiers.push(ItemAttributeModifierEntry {
307 attribute,
308 id,
309 amount,
310 operation,
311 slot,
312 display,
313 });
314 }
315
316 Some(Self { modifiers })
317 }
318}
319
320impl HashComponent for ItemAttributeModifiers {
321 fn hash_component(&self, hasher: &mut ComponentHasher) {
322 hasher.start_list();
323 for entry in &self.modifiers {
324 hasher.put_component_hash(entry);
325 }
326 hasher.end_list();
327 }
328}
329
330impl HashComponent for ItemAttributeModifierEntry {
331 fn hash_component(&self, hasher: &mut ComponentHasher) {
332 let mut entries = Vec::new();
333 push_hash_entry(&mut entries, "type", &self.attribute.key.to_string());
334 push_hash_entry(&mut entries, "id", &self.id.to_string());
335 push_hash_entry(&mut entries, "amount", &self.amount);
336 push_hash_entry(&mut entries, "operation", self.operation.name());
337 if self.slot != EquipmentSlotGroup::Any {
338 push_hash_entry(&mut entries, "slot", self.slot.name());
339 }
340 if !matches!(self.display, ItemAttributeModifierDisplay::Default) {
341 push_hash_entry(&mut entries, "display", &self.display);
342 }
343
344 sort_map_entries(&mut entries);
345 hasher.start_map();
346 for entry in &entries {
347 hasher.put_raw_bytes(&entry.key_bytes);
348 hasher.put_raw_bytes(&entry.value_bytes);
349 }
350 hasher.end_map();
351 }
352}
353
354impl HashComponent for ItemAttributeModifierDisplay {
355 fn hash_component(&self, hasher: &mut ComponentHasher) {
356 let mut entries = Vec::new();
357 push_hash_entry(&mut entries, "type", self.name());
358 if let Self::OverrideText(text) = self {
359 push_hash_entry(&mut entries, "value", text.as_ref());
360 }
361
362 sort_map_entries(&mut entries);
363 hasher.start_map();
364 for entry in &entries {
365 hasher.put_raw_bytes(&entry.key_bytes);
366 hasher.put_raw_bytes(&entry.value_bytes);
367 }
368 hasher.end_map();
369 }
370}
371
372fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
373 let mut key_hasher = ComponentHasher::new();
374 key_hasher.put_string(key);
375 let mut value_hasher = ComponentHasher::new();
376 value.hash_component(&mut value_hasher);
377 entries.push(HashEntry::new(key_hasher, value_hasher));
378}
379
380#[cfg(test)]
381mod tests {
382 use std::io::Cursor;
383
384 use simdnbt::FromNbtTag;
385 use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
386 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
387 use steel_utils::Identifier;
388 use steel_utils::codec::VarInt;
389 use steel_utils::serial::{ReadFrom, WriteTo};
390
391 use crate::attribute::AttributeModifierOperation;
392 use crate::equipment::{EquipmentSlot, EquipmentSlotGroup};
393 use crate::item_stack::ItemStack;
394 use crate::{RegistryEntry, init_vanilla_registry, vanilla_attributes, vanilla_items};
395
396 use super::{ItemAttributeModifierDisplay, ItemAttributeModifierEntry, ItemAttributeModifiers};
397
398 fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
399 let mut bytes = Vec::new();
400 tag.write(&mut bytes);
401 let borrowed =
402 read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
403 visitor(borrowed.as_tag())
404 }
405
406 fn modifier_nbt() -> NbtCompound {
407 let mut modifier = NbtCompound::new();
408 modifier.insert("type", "minecraft:armor");
409 modifier.insert("id", "minecraft:test");
410 modifier.insert("amount", 1_i32);
411 modifier.insert("operation", "add_value");
412 modifier
413 }
414
415 #[test]
416 fn generated_diamond_sword_has_main_hand_attack_modifiers() {
417 init_vanilla_registry();
418
419 let stack = ItemStack::new(&vanilla_items::DIAMOND_SWORD);
420 let modifiers = stack
421 .get_attribute_modifiers()
422 .expect("diamond sword should have attribute modifiers");
423 let main_hand_modifiers = modifiers
424 .for_slot(EquipmentSlot::MainHand)
425 .collect::<Vec<_>>();
426
427 assert_eq!(main_hand_modifiers.len(), 2);
428 assert!(main_hand_modifiers.iter().any(|modifier| {
429 modifier.attribute.key == vanilla_attributes::ATTACK_DAMAGE.key
430 && modifier.id == Identifier::vanilla_static("base_attack_damage")
431 && modifier.amount.to_bits() == 6.0f64.to_bits()
432 && modifier.operation == AttributeModifierOperation::AddValue
433 }));
434 assert!(main_hand_modifiers.iter().any(|modifier| {
435 modifier.attribute.key == vanilla_attributes::ATTACK_SPEED.key
436 && modifier.id == Identifier::vanilla_static("base_attack_speed")
437 && modifier.amount.to_bits() == (-2.4000000953674316f64).to_bits()
438 && modifier.operation == AttributeModifierOperation::AddValue
439 }));
440 assert!(modifiers.for_slot(EquipmentSlot::Head).next().is_none());
441 }
442
443 #[test]
444 fn generated_carved_pumpkin_has_hidden_head_modifier() {
445 init_vanilla_registry();
446
447 let stack = ItemStack::new(&vanilla_items::CARVED_PUMPKIN);
448 let modifiers = stack
449 .get_attribute_modifiers()
450 .expect("carved pumpkin should have attribute modifiers");
451 let head_modifiers = modifiers.for_slot(EquipmentSlot::Head).collect::<Vec<_>>();
452
453 assert_eq!(head_modifiers.len(), 1);
454 let modifier = head_modifiers[0];
455 assert_eq!(
456 modifier.attribute.key,
457 vanilla_attributes::WAYPOINT_TRANSMIT_RANGE.key
458 );
459 assert_eq!(
460 modifier.id,
461 Identifier::vanilla_static("waypoint_transmit_range_hide")
462 );
463 assert_eq!(
464 modifier.operation,
465 AttributeModifierOperation::AddMultipliedTotal
466 );
467 assert_eq!(modifier.display, ItemAttributeModifierDisplay::Hidden);
468 }
469
470 #[test]
471 fn unknown_attribute_modifier_slot_group_id_falls_back_to_any() {
472 init_vanilla_registry();
473
474 let mut bytes = Vec::new();
475 let Some(attribute_id) = vanilla_attributes::ARMOR.try_id() else {
476 panic!("armor attribute should be registered");
477 };
478 VarInt(attribute_id as i32).write(&mut bytes).unwrap();
479 Identifier::vanilla_static("test")
480 .write(&mut bytes)
481 .unwrap();
482 1.0_f64.write(&mut bytes).unwrap();
483 AttributeModifierOperation::AddValue
484 .write(&mut bytes)
485 .unwrap();
486 VarInt(999).write(&mut bytes).unwrap();
487 ItemAttributeModifierDisplay::Default
488 .write(&mut bytes)
489 .unwrap();
490
491 let entry = ItemAttributeModifierEntry::read(&mut Cursor::new(bytes.as_slice()))
492 .expect("unknown slot group id should fall back to any");
493
494 assert_eq!(entry.slot, EquipmentSlotGroup::Any);
495 }
496
497 #[test]
498 fn unknown_attribute_modifier_display_id_falls_back_to_default() {
499 let display = ItemAttributeModifierDisplay::read(&mut Cursor::new(&[99][..]))
500 .expect("unknown display id should fall back to default");
501
502 assert_eq!(display, ItemAttributeModifierDisplay::Default);
503 }
504
505 #[test]
506 fn attribute_modifier_nbt_coerces_amount_and_rejects_invalid_optional_fields() {
507 init_vanilla_registry();
508 let parsed = with_borrowed_tag(
509 NbtTag::List(NbtList::Compound(vec![modifier_nbt()])),
510 ItemAttributeModifiers::from_nbt_tag,
511 )
512 .expect("integer amount should decode through Codec.DOUBLE");
513 assert_eq!(parsed.modifiers[0].amount, 1.0);
514
515 let mut malformed = modifier_nbt();
516 malformed.insert("slot", 1);
517 assert!(
518 with_borrowed_tag(
519 NbtTag::List(NbtList::Compound(vec![malformed])),
520 ItemAttributeModifiers::from_nbt_tag,
521 )
522 .is_none()
523 );
524 }
525}