Skip to main content

steel_registry/data_components/components/
equippable.rs

1//! Equippable component for armor and equipment items.
2
3use std::io::{Cursor, Result, Write};
4use std::str::FromStr;
5
6use crate::{
7    RegistryHolderSet,
8    entity_type::{EntityType, EntityTypeRef},
9    equipment::EquipmentSlot,
10    sound_event::SoundEventHolder,
11    sound_events,
12};
13use steel_utils::{
14    Identifier,
15    codec::VarInt,
16    hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries},
17    nbt::NbtNumeric as _,
18    serial::{ReadFrom, WriteTo},
19};
20
21/// Entity types allowed to equip an item.
22pub type EquippableAllowedEntities = RegistryHolderSet<EntityType>;
23
24/// The equippable component data.
25#[derive(Debug, Clone, PartialEq)]
26pub struct Equippable {
27    pub slot: EquipmentSlot,
28    pub equip_sound: SoundEventHolder,
29    pub asset_id: Option<Identifier>,
30    pub camera_overlay: Option<Identifier>,
31    pub allowed_entities: Option<EquippableAllowedEntities>,
32    pub dispensable: bool,
33    pub swappable: bool,
34    pub damage_on_hurt: bool,
35    pub equip_on_interact: bool,
36    pub can_be_sheared: bool,
37    pub shearing_sound: SoundEventHolder,
38}
39
40impl Equippable {
41    /// Returns whether this item can be equipped by the entity type.
42    #[must_use]
43    pub fn can_be_equipped_by(&self, entity_type: EntityTypeRef) -> bool {
44        self.allowed_entities
45            .as_ref()
46            .is_none_or(|allowed| allowed.contains(entity_type))
47    }
48}
49
50impl WriteTo for Equippable {
51    fn write(&self, writer: &mut impl Write) -> Result<()> {
52        VarInt(self.slot.id()).write(writer)?;
53        self.equip_sound.write(writer)?;
54        self.asset_id.write(writer)?;
55        self.camera_overlay.write(writer)?;
56        self.allowed_entities.write(writer)?;
57        self.dispensable.write(writer)?;
58        self.swappable.write(writer)?;
59        self.damage_on_hurt.write(writer)?;
60        self.equip_on_interact.write(writer)?;
61        self.can_be_sheared.write(writer)?;
62        self.shearing_sound.write(writer)?;
63        Ok(())
64    }
65}
66
67impl ReadFrom for Equippable {
68    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
69        let slot_id = VarInt::read(data)?.0;
70        Ok(Self {
71            slot: EquipmentSlot::by_id(slot_id),
72            equip_sound: SoundEventHolder::read(data)?,
73            asset_id: Option::<Identifier>::read(data)?,
74            camera_overlay: Option::<Identifier>::read(data)?,
75            allowed_entities: Option::<EquippableAllowedEntities>::read(data)?,
76            dispensable: bool::read(data)?,
77            swappable: bool::read(data)?,
78            damage_on_hurt: bool::read(data)?,
79            equip_on_interact: bool::read(data)?,
80            can_be_sheared: bool::read(data)?,
81            shearing_sound: SoundEventHolder::read(data)?,
82        })
83    }
84}
85
86impl HashComponent for Equippable {
87    fn hash_component(&self, hasher: &mut ComponentHasher) {
88        let mut entries = Vec::new();
89        push_hash_entry(&mut entries, "slot", self.slot.name());
90        if self.equip_sound != SoundEventHolder::registry(&sound_events::ITEM_ARMOR_EQUIP_GENERIC) {
91            push_hash_entry(&mut entries, "equip_sound", &self.equip_sound);
92        }
93        if let Some(asset_id) = &self.asset_id {
94            push_hash_entry(&mut entries, "asset_id", &asset_id.to_string());
95        }
96        if let Some(camera_overlay) = &self.camera_overlay {
97            push_hash_entry(&mut entries, "camera_overlay", &camera_overlay.to_string());
98        }
99        if let Some(allowed_entities) = &self.allowed_entities {
100            push_hash_entry(&mut entries, "allowed_entities", allowed_entities);
101        }
102        if !self.dispensable {
103            push_hash_entry(&mut entries, "dispensable", &self.dispensable);
104        }
105        if !self.swappable {
106            push_hash_entry(&mut entries, "swappable", &self.swappable);
107        }
108        if !self.damage_on_hurt {
109            push_hash_entry(&mut entries, "damage_on_hurt", &self.damage_on_hurt);
110        }
111        if self.equip_on_interact {
112            push_hash_entry(&mut entries, "equip_on_interact", &self.equip_on_interact);
113        }
114        if self.can_be_sheared {
115            push_hash_entry(&mut entries, "can_be_sheared", &self.can_be_sheared);
116        }
117        if self.shearing_sound != SoundEventHolder::registry(&sound_events::ITEM_SHEARS_SNIP) {
118            push_hash_entry(&mut entries, "shearing_sound", &self.shearing_sound);
119        }
120
121        sort_map_entries(&mut entries);
122        hasher.start_map();
123        for entry in &entries {
124            hasher.put_raw_bytes(&entry.key_bytes);
125            hasher.put_raw_bytes(&entry.value_bytes);
126        }
127        hasher.end_map();
128    }
129}
130
131fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
132    let mut key_hasher = ComponentHasher::new();
133    key_hasher.put_string(key);
134    let mut value_hasher = ComponentHasher::new();
135    value.hash_component(&mut value_hasher);
136    entries.push(HashEntry::new(key_hasher, value_hasher));
137}
138
139impl simdnbt::ToNbtTag for Equippable {
140    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
141        use simdnbt::owned::{NbtCompound, NbtTag};
142
143        let mut compound = NbtCompound::new();
144        compound.insert("slot", self.slot.name());
145        if self.equip_sound != SoundEventHolder::registry(&sound_events::ITEM_ARMOR_EQUIP_GENERIC) {
146            compound.insert("equip_sound", self.equip_sound.to_nbt_tag());
147        }
148        if let Some(asset_id) = self.asset_id {
149            compound.insert("asset_id", asset_id.to_string());
150        }
151        if let Some(camera_overlay) = self.camera_overlay {
152            compound.insert("camera_overlay", camera_overlay.to_string());
153        }
154        if !self.dispensable {
155            compound.insert("dispensable", i8::from(self.dispensable));
156        }
157        if !self.swappable {
158            compound.insert("swappable", i8::from(self.swappable));
159        }
160        if !self.damage_on_hurt {
161            compound.insert("damage_on_hurt", i8::from(self.damage_on_hurt));
162        }
163        if self.equip_on_interact {
164            compound.insert("equip_on_interact", i8::from(self.equip_on_interact));
165        }
166        if self.can_be_sheared {
167            compound.insert("can_be_sheared", i8::from(self.can_be_sheared));
168        }
169        if self.shearing_sound != SoundEventHolder::registry(&sound_events::ITEM_SHEARS_SNIP) {
170            compound.insert("shearing_sound", self.shearing_sound.to_nbt_tag());
171        }
172        if let Some(allowed_entities) = self.allowed_entities {
173            compound.insert("allowed_entities", allowed_entities.to_nbt_tag());
174        }
175        NbtTag::Compound(compound)
176    }
177}
178
179impl simdnbt::FromNbtTag for Equippable {
180    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
181        let compound = tag.compound()?;
182        let slot_str = compound.get("slot")?.string()?.to_str();
183        let slot = EquipmentSlot::by_name(&slot_str)?;
184        let equip_sound = match compound.get("equip_sound") {
185            Some(tag) => SoundEventHolder::from_nbt_tag(tag)?,
186            None => SoundEventHolder::registry(&sound_events::ITEM_ARMOR_EQUIP_GENERIC),
187        };
188        let asset_id = match compound.get("asset_id") {
189            Some(tag) => Some(parse_identifier_nbt(tag)?),
190            None => None,
191        };
192        let camera_overlay = match compound.get("camera_overlay") {
193            Some(tag) => Some(parse_identifier_nbt(tag)?),
194            None => None,
195        };
196        let allowed_entities = match compound.get("allowed_entities") {
197            Some(tag) => Some(EquippableAllowedEntities::from_nbt_tag(tag)?),
198            None => None,
199        };
200        let dispensable = optional_bool(compound.get("dispensable"), true)?;
201        let swappable = optional_bool(compound.get("swappable"), true)?;
202        let damage_on_hurt = optional_bool(compound.get("damage_on_hurt"), true)?;
203        let equip_on_interact = optional_bool(compound.get("equip_on_interact"), false)?;
204        let can_be_sheared = optional_bool(compound.get("can_be_sheared"), false)?;
205        let shearing_sound = match compound.get("shearing_sound") {
206            Some(tag) => SoundEventHolder::from_nbt_tag(tag)?,
207            None => SoundEventHolder::registry(&sound_events::ITEM_SHEARS_SNIP),
208        };
209
210        Some(Self {
211            slot,
212            equip_sound,
213            asset_id,
214            camera_overlay,
215            allowed_entities,
216            dispensable,
217            swappable,
218            damage_on_hurt,
219            equip_on_interact,
220            can_be_sheared,
221            shearing_sound,
222        })
223    }
224}
225
226fn parse_identifier_nbt(tag: simdnbt::borrow::NbtTag) -> Option<Identifier> {
227    Identifier::from_str(&tag.string()?.to_str()).ok()
228}
229
230fn optional_bool(tag: Option<simdnbt::borrow::NbtTag<'_, '_>>, default: bool) -> Option<bool> {
231    match tag {
232        Some(tag) => tag.codec_bool(),
233        None => Some(default),
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use std::io::Cursor;
240
241    use super::{Equippable, EquippableAllowedEntities};
242    use crate::data_components::{ComponentData, vanilla_components::EQUIPPABLE};
243    use crate::init_vanilla_registry;
244    use crate::item_stack::ItemStack;
245    use crate::sound_event::SoundEventHolder;
246    use crate::sound_events;
247    use crate::vanilla_entities::{LLAMA, PIG, PLAYER, WOLF};
248    use crate::vanilla_entity_type_tags::EntityTypeTag;
249    use crate::vanilla_items;
250    use crate::{REGISTRY, RegistryExt};
251    use simdnbt::FromNbtTag;
252    use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
253    use simdnbt::owned::{NbtCompound, NbtTag};
254    use steel_utils::Identifier;
255    use steel_utils::serial::{ReadFrom, WriteTo};
256
257    fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
258        let mut bytes = Vec::new();
259        tag.write(&mut bytes);
260        let borrowed =
261            read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
262        visitor(borrowed.as_tag())
263    }
264
265    fn round_trip_equippable(equippable: &Equippable) -> Equippable {
266        let mut bytes = Vec::new();
267        equippable
268            .write(&mut bytes)
269            .expect("equippable should serialize");
270        Equippable::read(&mut Cursor::new(bytes.as_slice())).expect("equippable should deserialize")
271    }
272
273    #[test]
274    fn extracted_equippable_fields_gate_swapping_and_entity_types() {
275        init_vanilla_registry();
276
277        let pumpkin = ItemStack::new(&vanilla_items::CARVED_PUMPKIN);
278        let Some(pumpkin_equippable) = pumpkin.get_equippable() else {
279            panic!("carved pumpkin should have equippable data");
280        };
281        assert!(!pumpkin_equippable.swappable);
282        assert!(pumpkin_equippable.dispensable);
283        assert_eq!(
284            pumpkin_equippable.camera_overlay.as_ref(),
285            Some(&Identifier::vanilla_static("misc/pumpkinblur"))
286        );
287
288        let helmet = ItemStack::new(&vanilla_items::DIAMOND_HELMET);
289        let Some(helmet_equippable) = helmet.get_equippable() else {
290            panic!("diamond helmet should have equippable data");
291        };
292        assert!(helmet_equippable.dispensable);
293        assert!(helmet_equippable.swappable);
294        assert!(helmet_equippable.damage_on_hurt);
295        assert!(!helmet_equippable.can_be_sheared);
296        assert_eq!(
297            helmet_equippable.equip_sound,
298            SoundEventHolder::registry(&sound_events::ITEM_ARMOR_EQUIP_DIAMOND)
299        );
300        assert_eq!(
301            helmet_equippable.asset_id.as_ref(),
302            Some(&Identifier::vanilla_static("diamond"))
303        );
304        assert!(helmet_equippable.can_be_equipped_by(&PLAYER));
305
306        let saddle = ItemStack::new(&vanilla_items::SADDLE);
307        let Some(saddle_equippable) = saddle.get_equippable() else {
308            panic!("saddle should have equippable data");
309        };
310        assert!(saddle_equippable.dispensable);
311        assert!(saddle_equippable.equip_on_interact);
312        assert!(saddle_equippable.can_be_sheared);
313        assert_eq!(
314            saddle_equippable.shearing_sound,
315            SoundEventHolder::registry(&sound_events::ITEM_SADDLE_UNEQUIP)
316        );
317        assert_eq!(
318            saddle_equippable.asset_id.as_ref(),
319            Some(&Identifier::vanilla_static("saddle"))
320        );
321        assert_eq!(
322            saddle_equippable.allowed_entities,
323            Some(EquippableAllowedEntities::Tag(
324                EntityTypeTag::CAN_EQUIP_SADDLE
325            ))
326        );
327
328        let carpet = ItemStack::new(&vanilla_items::WHITE_CARPET);
329        let Some(carpet_equippable) = carpet.get_equippable() else {
330            panic!("carpet should have equippable data");
331        };
332        assert!(carpet_equippable.can_be_sheared);
333        assert_eq!(
334            carpet_equippable.shearing_sound,
335            SoundEventHolder::registry(&sound_events::ITEM_LLAMA_CARPET_UNEQUIP)
336        );
337        assert!(carpet_equippable.can_be_equipped_by(&LLAMA));
338        assert!(!carpet_equippable.can_be_equipped_by(&PIG));
339        assert!(!carpet_equippable.can_be_equipped_by(&PLAYER));
340
341        let wolf_armor = ItemStack::new(&vanilla_items::WOLF_ARMOR);
342        let Some(wolf_armor_equippable) = wolf_armor.get_equippable() else {
343            panic!("wolf armor should have equippable data");
344        };
345        assert!(wolf_armor_equippable.can_be_equipped_by(&WOLF));
346        assert!(!wolf_armor_equippable.can_be_equipped_by(&PLAYER));
347    }
348
349    #[test]
350    fn equippable_network_round_trips_tag_and_direct_holder_sets() {
351        init_vanilla_registry();
352
353        let saddle = ItemStack::new(&vanilla_items::SADDLE);
354        let Some(saddle_equippable) = saddle.get_equippable() else {
355            panic!("saddle should have equippable data");
356        };
357        assert_eq!(&round_trip_equippable(saddle_equippable), saddle_equippable);
358
359        let carpet = ItemStack::new(&vanilla_items::WHITE_CARPET);
360        let Some(carpet_equippable) = carpet.get_equippable() else {
361            panic!("carpet should have equippable data");
362        };
363        assert_eq!(&round_trip_equippable(carpet_equippable), carpet_equippable);
364    }
365
366    #[test]
367    fn equippable_hash_includes_vanilla_codec_fields() {
368        init_vanilla_registry();
369
370        let saddle = ItemStack::new(&vanilla_items::SADDLE);
371        let Some(saddle_equippable) = saddle.get_equippable() else {
372            panic!("saddle should have equippable data");
373        };
374        let helmet = ItemStack::new(&vanilla_items::DIAMOND_HELMET);
375        let Some(helmet_equippable) = helmet.get_equippable() else {
376            panic!("diamond helmet should have equippable data");
377        };
378
379        let component_type = REGISTRY
380            .data_components
381            .by_key(&EQUIPPABLE.key)
382            .expect("equippable component should be registered");
383        let saddle_hash = component_type
384            .compute_hash(&ComponentData::new(saddle_equippable.clone()))
385            .expect("equippable should have a persistent hash codec");
386        let helmet_hash = component_type
387            .compute_hash(&ComponentData::new(helmet_equippable.clone()))
388            .expect("equippable should have a persistent hash codec");
389        assert_ne!(saddle_hash, helmet_hash);
390    }
391
392    #[test]
393    fn equippable_nbt_defaults_only_missing_fields() {
394        init_vanilla_registry();
395        let mut compound = NbtCompound::new();
396        compound.insert("slot", "head");
397        compound.insert("dispensable", 0_i32);
398        let equippable = with_borrowed_tag(NbtTag::Compound(compound), Equippable::from_nbt_tag)
399            .expect("numeric boolean should parse");
400        assert!(!equippable.dispensable);
401        assert!(equippable.swappable);
402
403        let mut malformed = NbtCompound::new();
404        malformed.insert("slot", "head");
405        malformed.insert("camera_overlay", 1);
406        assert!(with_borrowed_tag(NbtTag::Compound(malformed), Equippable::from_nbt_tag).is_none());
407
408        let mut unknown_tag = NbtCompound::new();
409        unknown_tag.insert("slot", "head");
410        unknown_tag.insert("allowed_entities", "#minecraft:not_a_tag");
411        assert!(
412            with_borrowed_tag(NbtTag::Compound(unknown_tag), Equippable::from_nbt_tag).is_none()
413        );
414    }
415}