Skip to main content

steel_registry/data_components/components/
attribute_modifiers.rs

1//! Item attribute modifier component.
2#![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/// Vanilla `minecraft:attribute_modifiers` item component.
26#[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    /// Vanilla `ItemAttributeModifiers.compute`: fold the modifiers this item
54    /// grants for `attribute` in `slot` onto `base_value`, applying each entry's
55    /// operation. Used to approximate the attribute value an entity would have if
56    /// it wore the stack in that slot.
57    #[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/// A single item attribute modifier entry.
80#[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/// Tooltip display behavior for an item attribute modifier.
102#[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}