steel_registry/data_components/components/
equippable.rs1use 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
21pub type EquippableAllowedEntities = RegistryHolderSet<EntityType>;
23
24#[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 #[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}