1use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::codec::VarInt;
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11
12use crate::consume_effect::ConsumeEffectData;
13use crate::sound_event::SoundEventHolder;
14
15#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
17pub enum ItemUseAnimation {
18 None,
19 #[default]
20 Eat,
21 Drink,
22 Block,
23 Bow,
24 Trident,
25 Crossbow,
26 Spyglass,
27 TootHorn,
28 Brush,
29 Bundle,
30 Spear,
31}
32
33impl ItemUseAnimation {
34 #[must_use]
35 pub const fn id(self) -> i32 {
36 match self {
37 Self::None => 0,
38 Self::Eat => 1,
39 Self::Drink => 2,
40 Self::Block => 3,
41 Self::Bow => 4,
42 Self::Trident => 5,
43 Self::Crossbow => 6,
44 Self::Spyglass => 7,
45 Self::TootHorn => 8,
46 Self::Brush => 9,
47 Self::Bundle => 10,
48 Self::Spear => 11,
49 }
50 }
51
52 #[must_use]
53 pub const fn serialized_name(self) -> &'static str {
54 match self {
55 Self::None => "none",
56 Self::Eat => "eat",
57 Self::Drink => "drink",
58 Self::Block => "block",
59 Self::Bow => "bow",
60 Self::Trident => "trident",
61 Self::Crossbow => "crossbow",
62 Self::Spyglass => "spyglass",
63 Self::TootHorn => "toot_horn",
64 Self::Brush => "brush",
65 Self::Bundle => "bundle",
66 Self::Spear => "spear",
67 }
68 }
69
70 #[must_use]
71 pub const fn by_id(id: i32) -> Self {
72 match id {
73 1 => Self::Eat,
74 2 => Self::Drink,
75 3 => Self::Block,
76 4 => Self::Bow,
77 5 => Self::Trident,
78 6 => Self::Crossbow,
79 7 => Self::Spyglass,
80 8 => Self::TootHorn,
81 9 => Self::Brush,
82 10 => Self::Bundle,
83 11 => Self::Spear,
84 _ => Self::None,
85 }
86 }
87
88 const fn from_serialized_name(name: &str) -> Option<Self> {
89 match name {
90 "none" => Some(Self::None),
91 "eat" => Some(Self::Eat),
92 "drink" => Some(Self::Drink),
93 "block" => Some(Self::Block),
94 "bow" => Some(Self::Bow),
95 "trident" => Some(Self::Trident),
96 "crossbow" => Some(Self::Crossbow),
97 "spyglass" => Some(Self::Spyglass),
98 "toot_horn" => Some(Self::TootHorn),
99 "brush" => Some(Self::Brush),
100 "bundle" => Some(Self::Bundle),
101 "spear" => Some(Self::Spear),
102 _ => None,
103 }
104 }
105}
106
107impl WriteTo for ItemUseAnimation {
108 fn write(&self, writer: &mut impl Write) -> Result<()> {
109 VarInt(self.id()).write(writer)
110 }
111}
112
113impl ReadFrom for ItemUseAnimation {
114 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
115 Ok(Self::by_id(VarInt::read(data)?.0))
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct Consumable {
122 consume_seconds: f32,
123 animation: ItemUseAnimation,
124 sound: SoundEventHolder,
125 has_consume_particles: bool,
126 on_consume_effects: Vec<ConsumeEffectData>,
127}
128
129impl Consumable {
130 pub const DEFAULT_CONSUME_SECONDS: f32 = 1.6;
131
132 pub fn new(
133 consume_seconds: f32,
134 animation: ItemUseAnimation,
135 sound: SoundEventHolder,
136 has_consume_particles: bool,
137 on_consume_effects: Vec<ConsumeEffectData>,
138 ) -> Result<Self> {
139 if !is_non_negative_float(consume_seconds) {
140 return Err(Error::other("Consume duration must be non-negative"));
141 }
142 Ok(Self {
143 consume_seconds,
144 animation,
145 sound,
146 has_consume_particles,
147 on_consume_effects,
148 })
149 }
150
151 pub(crate) fn from_extracted(
152 consume_seconds: f32,
153 animation: ItemUseAnimation,
154 sound: SoundEventHolder,
155 has_consume_particles: bool,
156 on_consume_effects: Vec<ConsumeEffectData>,
157 ) -> Self {
158 assert!(
159 is_non_negative_float(consume_seconds),
160 "extracted consume duration must be non-negative"
161 );
162 Self {
163 consume_seconds,
164 animation,
165 sound,
166 has_consume_particles,
167 on_consume_effects,
168 }
169 }
170
171 #[must_use]
172 pub const fn consume_seconds(&self) -> f32 {
173 self.consume_seconds
174 }
175
176 #[must_use]
177 pub fn consume_ticks(&self) -> i32 {
178 (self.consume_seconds * 20.0) as i32
179 }
180
181 #[must_use]
182 pub const fn animation(&self) -> ItemUseAnimation {
183 self.animation
184 }
185
186 #[must_use]
187 pub const fn sound(&self) -> &SoundEventHolder {
188 &self.sound
189 }
190
191 #[must_use]
192 pub const fn has_consume_particles(&self) -> bool {
193 self.has_consume_particles
194 }
195
196 #[must_use]
197 pub fn on_consume_effects(&self) -> &[ConsumeEffectData] {
198 &self.on_consume_effects
199 }
200
201 fn to_nbt_tag_ref(&self) -> NbtTag {
202 let mut compound = NbtCompound::new();
203 if !float_equals(self.consume_seconds, Self::DEFAULT_CONSUME_SECONDS) {
204 compound.insert("consume_seconds", self.consume_seconds);
205 }
206 if self.animation != ItemUseAnimation::Eat {
207 compound.insert("animation", self.animation.serialized_name());
208 }
209 if !is_default_eat_sound(&self.sound) {
210 compound.insert("sound", self.sound.clone().to_nbt_tag());
211 }
212 if !self.has_consume_particles {
213 compound.insert("has_consume_particles", false);
214 }
215 if !self.on_consume_effects.is_empty() {
216 compound.insert(
217 "on_consume_effects",
218 NbtList::Compound(
219 self.on_consume_effects
220 .iter()
221 .map(|effect| match effect.to_nbt_tag_ref() {
222 NbtTag::Compound(compound) => compound,
223 _ => unreachable!("consume-effect codec always produces a compound"),
224 })
225 .collect(),
226 ),
227 );
228 }
229 NbtTag::Compound(compound)
230 }
231
232 fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
233 let compound = tag.compound()?;
234 let animation = match compound.get("animation") {
235 Some(tag) => ItemUseAnimation::from_serialized_name(&tag.string()?.to_string())?,
236 None => ItemUseAnimation::Eat,
237 };
238 let sound = match compound.get("sound") {
239 Some(tag) => SoundEventHolder::from_owned_nbt(tag)?,
240 None => default_eat_sound(),
241 };
242 let has_consume_particles = match compound.get("has_consume_particles") {
243 Some(tag) => tag.codec_bool()?,
244 None => true,
245 };
246 let on_consume_effects = match compound.get("on_consume_effects") {
247 Some(tag) => tag
248 .list()?
249 .as_nbt_tags()
250 .iter()
251 .map(ConsumeEffectData::from_owned_nbt)
252 .collect::<Option<Vec<_>>>()?,
253 None => Vec::new(),
254 };
255 Self::new(
256 optional_f32(
257 compound.get("consume_seconds"),
258 Self::DEFAULT_CONSUME_SECONDS,
259 )?,
260 animation,
261 sound,
262 has_consume_particles,
263 on_consume_effects,
264 )
265 .ok()
266 }
267}
268
269impl PartialEq for Consumable {
270 fn eq(&self, other: &Self) -> bool {
271 float_equals(self.consume_seconds, other.consume_seconds)
272 && self.animation == other.animation
273 && self.sound == other.sound
274 && self.has_consume_particles == other.has_consume_particles
275 && self.on_consume_effects == other.on_consume_effects
276 }
277}
278
279impl WriteTo for Consumable {
280 fn write(&self, writer: &mut impl Write) -> Result<()> {
281 self.consume_seconds.write(writer)?;
282 self.animation.write(writer)?;
283 self.sound.write(writer)?;
284 self.has_consume_particles.write(writer)?;
285 write_effect_list(&self.on_consume_effects, writer)
286 }
287}
288
289impl ReadFrom for Consumable {
290 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
291 Self::new(
292 f32::read(data)?,
293 ItemUseAnimation::read(data)?,
294 SoundEventHolder::read(data)?,
295 bool::read(data)?,
296 read_effect_list(data)?,
297 )
298 }
299}
300
301impl ToNbtTag for Consumable {
302 fn to_nbt_tag(self) -> NbtTag {
303 self.to_nbt_tag_ref()
304 }
305}
306
307impl FromNbtTag for Consumable {
308 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
309 Self::from_owned_nbt(&tag.to_owned())
310 }
311}
312
313impl HashComponent for Consumable {
314 fn hash_component(&self, hasher: &mut ComponentHasher) {
315 let mut entries = Vec::with_capacity(5);
316 if !float_equals(self.consume_seconds, Self::DEFAULT_CONSUME_SECONDS) {
317 push_hash_entry(&mut entries, "consume_seconds", &self.consume_seconds);
318 }
319 if self.animation != ItemUseAnimation::Eat {
320 push_hash_entry(&mut entries, "animation", self.animation.serialized_name());
321 }
322 if !is_default_eat_sound(&self.sound) {
323 push_hash_entry(&mut entries, "sound", &self.sound);
324 }
325 if !self.has_consume_particles {
326 push_hash_entry(&mut entries, "has_consume_particles", &false);
327 }
328 if !self.on_consume_effects.is_empty() {
329 push_hash_entry(
330 &mut entries,
331 "on_consume_effects",
332 &ConsumeEffectList(&self.on_consume_effects),
333 );
334 }
335 hash_entries(hasher, &mut entries);
336 }
337}
338
339#[derive(Debug, Default, Clone, PartialEq)]
341pub struct DeathProtection {
342 death_effects: Vec<ConsumeEffectData>,
343}
344
345impl DeathProtection {
346 #[must_use]
347 pub const fn new(death_effects: Vec<ConsumeEffectData>) -> Self {
348 Self { death_effects }
349 }
350
351 #[must_use]
352 pub fn death_effects(&self) -> &[ConsumeEffectData] {
353 &self.death_effects
354 }
355}
356
357impl WriteTo for DeathProtection {
358 fn write(&self, writer: &mut impl Write) -> Result<()> {
359 write_effect_list(&self.death_effects, writer)
360 }
361}
362
363impl ReadFrom for DeathProtection {
364 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
365 Ok(Self::new(read_effect_list(data)?))
366 }
367}
368
369impl ToNbtTag for DeathProtection {
370 fn to_nbt_tag(self) -> NbtTag {
371 let mut compound = NbtCompound::new();
372 if !self.death_effects.is_empty() {
373 compound.insert(
374 "death_effects",
375 NbtList::Compound(
376 self.death_effects
377 .iter()
378 .map(|effect| match effect.to_nbt_tag_ref() {
379 NbtTag::Compound(compound) => compound,
380 _ => unreachable!("consume-effect codec always produces a compound"),
381 })
382 .collect(),
383 ),
384 );
385 }
386 NbtTag::Compound(compound)
387 }
388}
389
390impl FromNbtTag for DeathProtection {
391 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
392 let tag = tag.to_owned();
393 let compound = tag.compound()?;
394 let death_effects = match compound.get("death_effects") {
395 Some(tag) => tag
396 .list()?
397 .as_nbt_tags()
398 .iter()
399 .map(ConsumeEffectData::from_owned_nbt)
400 .collect::<Option<Vec<_>>>()?,
401 None => Vec::new(),
402 };
403 Some(Self::new(death_effects))
404 }
405}
406
407impl HashComponent for DeathProtection {
408 fn hash_component(&self, hasher: &mut ComponentHasher) {
409 let mut entries = Vec::with_capacity(1);
410 if !self.death_effects.is_empty() {
411 push_hash_entry(
412 &mut entries,
413 "death_effects",
414 &ConsumeEffectList(&self.death_effects),
415 );
416 }
417 hash_entries(hasher, &mut entries);
418 }
419}
420
421struct ConsumeEffectList<'a>(&'a [ConsumeEffectData]);
422
423impl HashComponent for ConsumeEffectList<'_> {
424 fn hash_component(&self, hasher: &mut ComponentHasher) {
425 hasher.start_list();
426 for effect in self.0 {
427 hasher.put_component_hash(effect);
428 }
429 hasher.end_list();
430 }
431}
432
433fn default_eat_sound() -> SoundEventHolder {
434 SoundEventHolder::registry(&crate::sound_events::ENTITY_GENERIC_EAT)
435}
436
437fn is_default_eat_sound(sound: &SoundEventHolder) -> bool {
438 sound == &default_eat_sound()
439}
440
441fn write_effect_list(effects: &[ConsumeEffectData], writer: &mut impl Write) -> Result<()> {
442 let count = i32::try_from(effects.len())
443 .map_err(|_| Error::other("Consume effect list is too large"))?;
444 VarInt(count).write(writer)?;
445 for effect in effects {
446 effect.write(writer)?;
447 }
448 Ok(())
449}
450
451fn read_effect_list(data: &mut Cursor<&[u8]>) -> Result<Vec<ConsumeEffectData>> {
452 let count = VarInt::read(data)?.0;
453 let count = usize::try_from(count)
454 .map_err(|_| Error::other(format!("Negative consume effect count: {count}")))?;
455 let mut effects = Vec::with_capacity(count.min(65_536));
456 for _ in 0..count {
457 effects.push(ConsumeEffectData::read(data)?);
458 }
459 Ok(effects)
460}
461
462const fn float_equals(left: f32, right: f32) -> bool {
463 (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
464}
465
466const fn is_non_negative_float(value: f32) -> bool {
467 value.is_finite() && !value.is_sign_negative()
468}
469
470fn optional_f32(tag: Option<&NbtTag>, default: f32) -> Option<f32> {
471 match tag {
472 Some(tag) => tag.codec_f32(),
473 None => Some(default),
474 }
475}
476
477fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
478 let mut key_hasher = ComponentHasher::new();
479 key_hasher.put_string(key);
480 let mut value_hasher = ComponentHasher::new();
481 value.hash_component(&mut value_hasher);
482 entries.push(HashEntry::new(key_hasher, value_hasher));
483}
484
485fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
486 sort_map_entries(entries);
487 hasher.start_map();
488 for entry in entries {
489 hasher.put_raw_bytes(&entry.key_bytes);
490 hasher.put_raw_bytes(&entry.value_bytes);
491 }
492 hasher.end_map();
493}
494
495#[cfg(test)]
496mod tests {
497 use std::io::Cursor;
498
499 use steel_utils::codec::VarInt;
500 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
501
502 use super::{Consumable, DeathProtection, ItemUseAnimation};
503 use crate::consume_effect::{
504 ApplyStatusEffectsConsumeEffect, ClearAllStatusEffectsConsumeEffect,
505 RemoveStatusEffectsConsumeEffect, TeleportRandomlyConsumeEffect,
506 };
507 use crate::data_components::vanilla_components::{CONSUMABLE, DEATH_PROTECTION};
508 use crate::init_vanilla_registry;
509 use crate::{REGISTRY, RegistryExt};
510
511 fn parse<T: simdnbt::FromNbtTag>(tag: simdnbt::owned::NbtTag) -> Option<T> {
512 let mut bytes = Vec::new();
513 tag.write(&mut bytes);
514 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
515 T::from_nbt_tag(borrowed.as_tag())
516 }
517
518 fn assert_round_trip<T>(value: &T)
519 where
520 T: Clone
521 + PartialEq
522 + std::fmt::Debug
523 + simdnbt::ToNbtTag
524 + simdnbt::FromNbtTag
525 + steel_utils::serial::WriteTo
526 + steel_utils::serial::ReadFrom,
527 {
528 assert_eq!(parse(value.clone().to_nbt_tag()), Some(value.clone()));
529 let mut network = Vec::new();
530 value.write(&mut network).expect("component should encode");
531 assert_eq!(
532 T::read(&mut Cursor::new(network.as_slice())).expect("component should decode"),
533 value.clone()
534 );
535 }
536
537 #[test]
538 fn extracted_consumables_keep_typed_effects_and_round_trip() {
539 init_vanilla_registry();
540 let golden_apple = REGISTRY
541 .items
542 .by_key(&steel_utils::Identifier::vanilla_static("golden_apple"))
543 .expect("golden apple should be registered")
544 .components
545 .get(CONSUMABLE)
546 .expect("golden apple should be consumable");
547 let apply = golden_apple.on_consume_effects()[0]
548 .downcast_ref::<ApplyStatusEffectsConsumeEffect>()
549 .expect("golden apple should apply effects");
550 assert_eq!(apply.effects().len(), 2);
551 assert_round_trip(&golden_apple);
552
553 let milk = REGISTRY
554 .items
555 .by_key(&steel_utils::Identifier::vanilla_static("milk_bucket"))
556 .expect("milk bucket should be registered")
557 .components
558 .get(CONSUMABLE)
559 .expect("milk should be consumable");
560 assert_eq!(milk.animation(), ItemUseAnimation::Drink);
561 assert_eq!(
562 milk.consume_ticks(),
563 (milk.consume_seconds() * 20.0) as i32,
564 "consume_ticks must match vanilla (int)(seconds * 20)"
565 );
566 assert!(!milk.has_consume_particles());
567 assert!(
568 milk.on_consume_effects()[0]
569 .downcast_ref::<ClearAllStatusEffectsConsumeEffect>()
570 .is_some()
571 );
572 assert_round_trip(&milk);
573
574 let honey = REGISTRY
575 .items
576 .by_key(&steel_utils::Identifier::vanilla_static("honey_bottle"))
577 .expect("honey bottle should be registered")
578 .components
579 .get(CONSUMABLE)
580 .expect("honey should be consumable");
581 assert!(
582 honey.on_consume_effects()[0]
583 .downcast_ref::<RemoveStatusEffectsConsumeEffect>()
584 .is_some()
585 );
586 assert_round_trip(&honey);
587 }
588
589 #[test]
590 fn extracted_totem_death_protection_round_trips() {
591 init_vanilla_registry();
592 let totem = REGISTRY
593 .items
594 .by_key(&steel_utils::Identifier::vanilla_static("totem_of_undying"))
595 .expect("totem should be registered")
596 .components
597 .get(DEATH_PROTECTION)
598 .expect("totem should provide death protection");
599 assert_eq!(totem.death_effects().len(), 2);
600 assert!(
601 totem.death_effects()[0]
602 .downcast_ref::<ClearAllStatusEffectsConsumeEffect>()
603 .is_some()
604 );
605 assert_round_trip::<DeathProtection>(&totem);
606 }
607
608 #[test]
609 fn network_animation_ids_use_vanilla_zero_fallback() {
610 let mut bytes = Vec::new();
611 VarInt(i32::MAX)
612 .write(&mut bytes)
613 .expect("test id should encode");
614 assert_eq!(
615 ItemUseAnimation::read(&mut Cursor::new(bytes.as_slice()))
616 .expect("unknown id should decode"),
617 ItemUseAnimation::None
618 );
619 assert!(
620 Consumable::new(
621 -1.0,
622 ItemUseAnimation::Eat,
623 crate::sound_event::SoundEventHolder::Direct {
624 sound_id: steel_utils::Identifier::vanilla_static("test"),
625 fixed_range: None,
626 },
627 true,
628 Vec::new(),
629 )
630 .is_err()
631 );
632 }
633
634 #[test]
635 fn persistent_float_ranges_match_vanilla_float_compare_bounds() {
636 let sound = || crate::sound_event::SoundEventHolder::Direct {
637 sound_id: steel_utils::Identifier::vanilla_static("test"),
638 fixed_range: None,
639 };
640 for invalid in [-0.0, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
641 assert!(
642 Consumable::new(invalid, ItemUseAnimation::Eat, sound(), true, Vec::new()).is_err()
643 );
644 }
645 assert!(Consumable::new(0.0, ItemUseAnimation::Eat, sound(), true, Vec::new()).is_ok());
646
647 for invalid in [0.0, -0.0, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
648 assert!(TeleportRandomlyConsumeEffect::new(invalid).is_err());
649 }
650 assert!(TeleportRandomlyConsumeEffect::new(f32::MAX).is_ok());
651
652 for invalid in [-0.0, 1.1, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
653 assert!(ApplyStatusEffectsConsumeEffect::new(Vec::new(), invalid).is_err());
654 }
655 assert!(ApplyStatusEffectsConsumeEffect::new(Vec::new(), 0.0).is_ok());
656 assert!(ApplyStatusEffectsConsumeEffect::new(Vec::new(), 1.0).is_ok());
657 }
658}