Skip to main content

steel_registry/data_components/components/
blocks_attacks.rs

1//! Vanilla `minecraft:blocks_attacks` item component.
2
3use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_math::DEGREE_90;
8use steel_utils::codec::VarInt;
9use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
10use steel_utils::nbt::NbtNumeric as _;
11use steel_utils::serial::{ReadFrom, WriteTo};
12
13use crate::RegistryHolderSet;
14use crate::damage_type::DamageType;
15use crate::sound_event::SoundEventHolder;
16
17/// One directional damage-reduction rule.
18#[derive(Debug, Clone)]
19pub struct DamageReduction {
20    horizontal_blocking_angle: f32,
21    damage_types: Option<RegistryHolderSet<DamageType>>,
22    base: f32,
23    factor: f32,
24}
25
26impl DamageReduction {
27    pub const DEFAULT_HORIZONTAL_BLOCKING_ANGLE: f32 = DEGREE_90;
28
29    pub fn new(
30        horizontal_blocking_angle: f32,
31        damage_types: Option<RegistryHolderSet<DamageType>>,
32        base: f32,
33        factor: f32,
34    ) -> Result<Self> {
35        if !is_positive_float(horizontal_blocking_angle) {
36            return Err(Error::other("Horizontal blocking angle must be positive"));
37        }
38        Ok(Self {
39            horizontal_blocking_angle,
40            damage_types,
41            base,
42            factor,
43        })
44    }
45
46    #[must_use]
47    pub const fn default_rule() -> Self {
48        Self {
49            horizontal_blocking_angle: Self::DEFAULT_HORIZONTAL_BLOCKING_ANGLE,
50            damage_types: None,
51            base: 0.0,
52            factor: 1.0,
53        }
54    }
55
56    #[must_use]
57    pub const fn horizontal_blocking_angle(&self) -> f32 {
58        self.horizontal_blocking_angle
59    }
60
61    #[must_use]
62    pub const fn damage_types(&self) -> Option<&RegistryHolderSet<DamageType>> {
63        self.damage_types.as_ref()
64    }
65
66    #[must_use]
67    pub const fn base(&self) -> f32 {
68        self.base
69    }
70
71    #[must_use]
72    pub const fn factor(&self) -> f32 {
73        self.factor
74    }
75
76    fn to_nbt_compound(&self) -> NbtCompound {
77        let mut compound = NbtCompound::new();
78        if !float_equals(
79            self.horizontal_blocking_angle,
80            Self::DEFAULT_HORIZONTAL_BLOCKING_ANGLE,
81        ) {
82            compound.insert("horizontal_blocking_angle", self.horizontal_blocking_angle);
83        }
84        if let Some(damage_types) = &self.damage_types {
85            compound.insert("type", damage_types.clone().to_nbt_tag());
86        }
87        compound.insert("base", self.base);
88        compound.insert("factor", self.factor);
89        compound
90    }
91
92    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
93        let compound = tag.compound()?;
94        let angle = optional_f32(
95            compound.get("horizontal_blocking_angle"),
96            Self::DEFAULT_HORIZONTAL_BLOCKING_ANGLE,
97        )?;
98        let damage_types = match compound.get("type") {
99            Some(tag) => Some(RegistryHolderSet::from_owned_nbt(tag)?),
100            None => None,
101        };
102        Self::new(
103            angle,
104            damage_types,
105            compound.get("base")?.codec_f32()?,
106            compound.get("factor")?.codec_f32()?,
107        )
108        .ok()
109    }
110}
111
112impl PartialEq for DamageReduction {
113    fn eq(&self, other: &Self) -> bool {
114        float_equals(
115            self.horizontal_blocking_angle,
116            other.horizontal_blocking_angle,
117        ) && self.damage_types == other.damage_types
118            && float_equals(self.base, other.base)
119            && float_equals(self.factor, other.factor)
120    }
121}
122
123impl WriteTo for DamageReduction {
124    fn write(&self, writer: &mut impl Write) -> Result<()> {
125        self.horizontal_blocking_angle.write(writer)?;
126        self.damage_types.is_some().write(writer)?;
127        if let Some(damage_types) = &self.damage_types {
128            damage_types.write(writer)?;
129        }
130        self.base.write(writer)?;
131        self.factor.write(writer)
132    }
133}
134
135impl ReadFrom for DamageReduction {
136    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
137        let angle = f32::read(data)?;
138        let damage_types = if bool::read(data)? {
139            Some(RegistryHolderSet::read(data)?)
140        } else {
141            None
142        };
143        Self::new(angle, damage_types, f32::read(data)?, f32::read(data)?)
144    }
145}
146
147impl HashComponent for DamageReduction {
148    fn hash_component(&self, hasher: &mut ComponentHasher) {
149        let mut entries = Vec::with_capacity(4);
150        if !float_equals(
151            self.horizontal_blocking_angle,
152            Self::DEFAULT_HORIZONTAL_BLOCKING_ANGLE,
153        ) {
154            push_hash_entry(
155                &mut entries,
156                "horizontal_blocking_angle",
157                &self.horizontal_blocking_angle,
158            );
159        }
160        if let Some(damage_types) = &self.damage_types {
161            push_hash_entry(&mut entries, "type", damage_types);
162        }
163        push_hash_entry(&mut entries, "base", &self.base);
164        push_hash_entry(&mut entries, "factor", &self.factor);
165        hash_entries(hasher, &mut entries);
166    }
167}
168
169/// Durability damage applied after blocking an attack.
170#[derive(Debug, Clone, Copy)]
171pub struct ItemDamageFunction {
172    threshold: f32,
173    base: f32,
174    factor: f32,
175}
176
177impl ItemDamageFunction {
178    pub const DEFAULT: Self = Self {
179        threshold: 1.0,
180        base: 0.0,
181        factor: 1.0,
182    };
183
184    pub fn new(threshold: f32, base: f32, factor: f32) -> Result<Self> {
185        if !is_non_negative_float(threshold) {
186            return Err(Error::other(
187                "Block item-damage threshold must be non-negative",
188            ));
189        }
190        Ok(Self {
191            threshold,
192            base,
193            factor,
194        })
195    }
196
197    pub(crate) const fn from_extracted(threshold: f32, base: f32, factor: f32) -> Self {
198        assert!(
199            is_non_negative_float(threshold),
200            "extracted block item-damage threshold must be non-negative"
201        );
202        Self {
203            threshold,
204            base,
205            factor,
206        }
207    }
208
209    #[must_use]
210    pub const fn threshold(self) -> f32 {
211        self.threshold
212    }
213
214    #[must_use]
215    pub const fn base(self) -> f32 {
216        self.base
217    }
218
219    #[must_use]
220    pub const fn factor(self) -> f32 {
221        self.factor
222    }
223
224    fn to_nbt_compound(self) -> NbtCompound {
225        let mut compound = NbtCompound::new();
226        compound.insert("threshold", self.threshold);
227        compound.insert("base", self.base);
228        compound.insert("factor", self.factor);
229        compound
230    }
231
232    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
233        let compound = tag.compound()?;
234        Self::new(
235            compound.get("threshold")?.codec_f32()?,
236            compound.get("base")?.codec_f32()?,
237            compound.get("factor")?.codec_f32()?,
238        )
239        .ok()
240    }
241}
242
243impl PartialEq for ItemDamageFunction {
244    fn eq(&self, other: &Self) -> bool {
245        float_equals(self.threshold, other.threshold)
246            && float_equals(self.base, other.base)
247            && float_equals(self.factor, other.factor)
248    }
249}
250
251impl WriteTo for ItemDamageFunction {
252    fn write(&self, writer: &mut impl Write) -> Result<()> {
253        self.threshold.write(writer)?;
254        self.base.write(writer)?;
255        self.factor.write(writer)
256    }
257}
258
259impl ReadFrom for ItemDamageFunction {
260    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
261        Self::new(f32::read(data)?, f32::read(data)?, f32::read(data)?)
262    }
263}
264
265impl HashComponent for ItemDamageFunction {
266    fn hash_component(&self, hasher: &mut ComponentHasher) {
267        let mut entries = Vec::with_capacity(3);
268        push_hash_entry(&mut entries, "threshold", &self.threshold);
269        push_hash_entry(&mut entries, "base", &self.base);
270        push_hash_entry(&mut entries, "factor", &self.factor);
271        hash_entries(hasher, &mut entries);
272    }
273}
274
275/// Complete blocking behavior configured on an item.
276#[derive(Debug, Clone)]
277pub struct BlocksAttacks {
278    block_delay_seconds: f32,
279    disable_cooldown_scale: f32,
280    damage_reductions: Vec<DamageReduction>,
281    item_damage: ItemDamageFunction,
282    bypassed_by: Option<RegistryHolderSet<DamageType>>,
283    block_sound: Option<SoundEventHolder>,
284    disabled_sound: Option<SoundEventHolder>,
285}
286
287impl BlocksAttacks {
288    pub fn new(
289        block_delay_seconds: f32,
290        disable_cooldown_scale: f32,
291        damage_reductions: Vec<DamageReduction>,
292        item_damage: ItemDamageFunction,
293        bypassed_by: Option<RegistryHolderSet<DamageType>>,
294        block_sound: Option<SoundEventHolder>,
295        disabled_sound: Option<SoundEventHolder>,
296    ) -> Result<Self> {
297        if !is_non_negative_float(block_delay_seconds) {
298            return Err(Error::other("Block delay must be non-negative"));
299        }
300        if !is_non_negative_float(disable_cooldown_scale) {
301            return Err(Error::other("Disable cooldown scale must be non-negative"));
302        }
303        Ok(Self {
304            block_delay_seconds,
305            disable_cooldown_scale,
306            damage_reductions,
307            item_damage,
308            bypassed_by,
309            block_sound,
310            disabled_sound,
311        })
312    }
313
314    pub(crate) fn from_extracted_shield(
315        block_delay_seconds: f32,
316        item_damage: ItemDamageFunction,
317        bypassed_by: RegistryHolderSet<DamageType>,
318        block_sound: SoundEventHolder,
319        disabled_sound: SoundEventHolder,
320    ) -> Self {
321        assert!(
322            is_non_negative_float(block_delay_seconds),
323            "extracted shield block delay must be non-negative"
324        );
325        Self {
326            block_delay_seconds,
327            disable_cooldown_scale: 1.0,
328            damage_reductions: vec![DamageReduction::default_rule()],
329            item_damage,
330            bypassed_by: Some(bypassed_by),
331            block_sound: Some(block_sound),
332            disabled_sound: Some(disabled_sound),
333        }
334    }
335
336    #[must_use]
337    pub const fn block_delay_seconds(&self) -> f32 {
338        self.block_delay_seconds
339    }
340
341    #[must_use]
342    pub const fn disable_cooldown_scale(&self) -> f32 {
343        self.disable_cooldown_scale
344    }
345
346    #[must_use]
347    pub fn damage_reductions(&self) -> &[DamageReduction] {
348        &self.damage_reductions
349    }
350
351    #[must_use]
352    pub const fn item_damage(&self) -> ItemDamageFunction {
353        self.item_damage
354    }
355
356    #[must_use]
357    pub const fn bypassed_by(&self) -> Option<&RegistryHolderSet<DamageType>> {
358        self.bypassed_by.as_ref()
359    }
360
361    #[must_use]
362    pub const fn block_sound(&self) -> Option<&SoundEventHolder> {
363        self.block_sound.as_ref()
364    }
365
366    #[must_use]
367    pub const fn disabled_sound(&self) -> Option<&SoundEventHolder> {
368        self.disabled_sound.as_ref()
369    }
370
371    fn to_nbt_tag_ref(&self) -> NbtTag {
372        let mut compound = NbtCompound::new();
373        if !float_equals(self.block_delay_seconds, 0.0) {
374            compound.insert("block_delay_seconds", self.block_delay_seconds);
375        }
376        if !float_equals(self.disable_cooldown_scale, 1.0) {
377            compound.insert("disable_cooldown_scale", self.disable_cooldown_scale);
378        }
379        if self.damage_reductions != [DamageReduction::default_rule()] {
380            compound.insert(
381                "damage_reductions",
382                NbtList::Compound(
383                    self.damage_reductions
384                        .iter()
385                        .map(DamageReduction::to_nbt_compound)
386                        .collect(),
387                ),
388            );
389        }
390        if self.item_damage != ItemDamageFunction::DEFAULT {
391            compound.insert(
392                "item_damage",
393                NbtTag::Compound(self.item_damage.to_nbt_compound()),
394            );
395        }
396        if let Some(bypassed_by) = &self.bypassed_by {
397            compound.insert("bypassed_by", bypassed_by.clone().to_nbt_tag());
398        }
399        if let Some(block_sound) = &self.block_sound {
400            compound.insert("block_sound", block_sound.clone().to_nbt_tag());
401        }
402        if let Some(disabled_sound) = &self.disabled_sound {
403            compound.insert("disabled_sound", disabled_sound.clone().to_nbt_tag());
404        }
405        NbtTag::Compound(compound)
406    }
407
408    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
409        let compound = tag.compound()?;
410        let damage_reductions = match compound.get("damage_reductions") {
411            Some(tag) => tag
412                .list()?
413                .as_nbt_tags()
414                .iter()
415                .map(DamageReduction::from_owned_nbt)
416                .collect::<Option<Vec<_>>>()?,
417            None => vec![DamageReduction::default_rule()],
418        };
419        let item_damage = match compound.get("item_damage") {
420            Some(tag) => ItemDamageFunction::from_owned_nbt(tag)?,
421            None => ItemDamageFunction::DEFAULT,
422        };
423        let bypassed_by = match compound.get("bypassed_by") {
424            Some(tag) => Some(RegistryHolderSet::from_owned_nbt(tag)?),
425            None => None,
426        };
427        let block_sound = match compound.get("block_sound") {
428            Some(tag) => Some(SoundEventHolder::from_owned_nbt(tag)?),
429            None => None,
430        };
431        let disabled_sound = match compound.get("disabled_sound") {
432            Some(tag) => Some(SoundEventHolder::from_owned_nbt(tag)?),
433            None => None,
434        };
435        Self::new(
436            optional_f32(compound.get("block_delay_seconds"), 0.0)?,
437            optional_f32(compound.get("disable_cooldown_scale"), 1.0)?,
438            damage_reductions,
439            item_damage,
440            bypassed_by,
441            block_sound,
442            disabled_sound,
443        )
444        .ok()
445    }
446}
447
448impl PartialEq for BlocksAttacks {
449    fn eq(&self, other: &Self) -> bool {
450        float_equals(self.block_delay_seconds, other.block_delay_seconds)
451            && float_equals(self.disable_cooldown_scale, other.disable_cooldown_scale)
452            && self.damage_reductions == other.damage_reductions
453            && self.item_damage == other.item_damage
454            && self.bypassed_by == other.bypassed_by
455            && self.block_sound == other.block_sound
456            && self.disabled_sound == other.disabled_sound
457    }
458}
459
460impl WriteTo for BlocksAttacks {
461    fn write(&self, writer: &mut impl Write) -> Result<()> {
462        self.block_delay_seconds.write(writer)?;
463        self.disable_cooldown_scale.write(writer)?;
464        write_count(self.damage_reductions.len(), writer)?;
465        for reduction in &self.damage_reductions {
466            reduction.write(writer)?;
467        }
468        self.item_damage.write(writer)?;
469        self.bypassed_by.is_some().write(writer)?;
470        if let Some(bypassed_by) = &self.bypassed_by {
471            bypassed_by.write(writer)?;
472        }
473        self.block_sound.is_some().write(writer)?;
474        if let Some(block_sound) = &self.block_sound {
475            block_sound.write(writer)?;
476        }
477        self.disabled_sound.is_some().write(writer)?;
478        if let Some(disabled_sound) = &self.disabled_sound {
479            disabled_sound.write(writer)?;
480        }
481        Ok(())
482    }
483}
484
485impl ReadFrom for BlocksAttacks {
486    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
487        let block_delay_seconds = f32::read(data)?;
488        let disable_cooldown_scale = f32::read(data)?;
489        let count = read_count(data)?;
490        let mut damage_reductions = Vec::with_capacity(count.min(65_536));
491        for _ in 0..count {
492            damage_reductions.push(DamageReduction::read(data)?);
493        }
494        let item_damage = ItemDamageFunction::read(data)?;
495        let bypassed_by = if bool::read(data)? {
496            Some(RegistryHolderSet::read(data)?)
497        } else {
498            None
499        };
500        let block_sound = if bool::read(data)? {
501            Some(SoundEventHolder::read(data)?)
502        } else {
503            None
504        };
505        let disabled_sound = if bool::read(data)? {
506            Some(SoundEventHolder::read(data)?)
507        } else {
508            None
509        };
510        Self::new(
511            block_delay_seconds,
512            disable_cooldown_scale,
513            damage_reductions,
514            item_damage,
515            bypassed_by,
516            block_sound,
517            disabled_sound,
518        )
519    }
520}
521
522impl ToNbtTag for BlocksAttacks {
523    fn to_nbt_tag(self) -> NbtTag {
524        self.to_nbt_tag_ref()
525    }
526}
527
528impl FromNbtTag for BlocksAttacks {
529    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
530        Self::from_owned_nbt(&tag.to_owned())
531    }
532}
533
534impl HashComponent for BlocksAttacks {
535    fn hash_component(&self, hasher: &mut ComponentHasher) {
536        let mut entries = Vec::with_capacity(7);
537        if !float_equals(self.block_delay_seconds, 0.0) {
538            push_hash_entry(
539                &mut entries,
540                "block_delay_seconds",
541                &self.block_delay_seconds,
542            );
543        }
544        if !float_equals(self.disable_cooldown_scale, 1.0) {
545            push_hash_entry(
546                &mut entries,
547                "disable_cooldown_scale",
548                &self.disable_cooldown_scale,
549            );
550        }
551        if self.damage_reductions != [DamageReduction::default_rule()] {
552            push_hash_entry(
553                &mut entries,
554                "damage_reductions",
555                &DamageReductionList(&self.damage_reductions),
556            );
557        }
558        if self.item_damage != ItemDamageFunction::DEFAULT {
559            push_hash_entry(&mut entries, "item_damage", &self.item_damage);
560        }
561        if let Some(bypassed_by) = &self.bypassed_by {
562            push_hash_entry(&mut entries, "bypassed_by", bypassed_by);
563        }
564        if let Some(block_sound) = &self.block_sound {
565            push_hash_entry(&mut entries, "block_sound", block_sound);
566        }
567        if let Some(disabled_sound) = &self.disabled_sound {
568            push_hash_entry(&mut entries, "disabled_sound", disabled_sound);
569        }
570        hash_entries(hasher, &mut entries);
571    }
572}
573
574struct DamageReductionList<'a>(&'a [DamageReduction]);
575
576impl HashComponent for DamageReductionList<'_> {
577    fn hash_component(&self, hasher: &mut ComponentHasher) {
578        hasher.start_list();
579        for reduction in self.0 {
580            hasher.put_component_hash(reduction);
581        }
582        hasher.end_list();
583    }
584}
585
586const fn float_equals(left: f32, right: f32) -> bool {
587    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
588}
589
590const fn is_non_negative_float(value: f32) -> bool {
591    value.is_finite() && !value.is_sign_negative()
592}
593
594const fn is_positive_float(value: f32) -> bool {
595    value > 0.0 && value <= f32::MAX
596}
597
598fn optional_f32(tag: Option<&NbtTag>, default: f32) -> Option<f32> {
599    match tag {
600        Some(tag) => tag.codec_f32(),
601        None => Some(default),
602    }
603}
604
605fn write_count(count: usize, writer: &mut impl Write) -> Result<()> {
606    let count =
607        i32::try_from(count).map_err(|_| Error::other("Damage reduction list too large"))?;
608    VarInt(count).write(writer)
609}
610
611fn read_count(data: &mut Cursor<&[u8]>) -> Result<usize> {
612    let count = VarInt::read(data)?.0;
613    usize::try_from(count)
614        .map_err(|_| Error::other(format!("Negative damage reduction count: {count}")))
615}
616
617fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
618    let mut key_hasher = ComponentHasher::new();
619    key_hasher.put_string(key);
620    let mut value_hasher = ComponentHasher::new();
621    value.hash_component(&mut value_hasher);
622    entries.push(HashEntry::new(key_hasher, value_hasher));
623}
624
625fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
626    sort_map_entries(entries);
627    hasher.start_map();
628    for entry in entries {
629        hasher.put_raw_bytes(&entry.key_bytes);
630        hasher.put_raw_bytes(&entry.value_bytes);
631    }
632    hasher.end_map();
633}
634
635#[cfg(test)]
636mod tests {
637    use std::io::Cursor;
638
639    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
640    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
641
642    use super::{BlocksAttacks, DamageReduction, ItemDamageFunction};
643    use crate::data_components::vanilla_components::BLOCKS_ATTACKS;
644    use crate::init_vanilla_registry;
645    use crate::{REGISTRY, RegistryExt};
646
647    fn parse(tag: simdnbt::owned::NbtTag) -> Option<BlocksAttacks> {
648        let mut bytes = Vec::new();
649        tag.write(&mut bytes);
650        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
651        BlocksAttacks::from_nbt_tag(borrowed.as_tag())
652    }
653
654    #[test]
655    fn shield_component_round_trips_both_codecs() {
656        init_vanilla_registry();
657        let shield = REGISTRY
658            .items
659            .by_key(&steel_utils::Identifier::vanilla_static("shield"))
660            .expect("shield should be registered");
661        let value = shield
662            .components
663            .get(BLOCKS_ATTACKS)
664            .expect("shield should block attacks");
665        assert_eq!(value.block_delay_seconds(), 0.25);
666        assert_eq!(
667            value.item_damage(),
668            ItemDamageFunction::new(3.0, 1.0, 1.0).expect("valid item damage")
669        );
670
671        let nbt = value.clone().to_nbt_tag();
672        assert_eq!(parse(nbt), Some(value.clone()));
673        let mut network = Vec::new();
674        value
675            .write(&mut network)
676            .expect("blocks_attacks should encode");
677        assert_eq!(
678            BlocksAttacks::read(&mut Cursor::new(network.as_slice()))
679                .expect("blocks_attacks should decode"),
680            value
681        );
682    }
683
684    #[test]
685    fn persistence_constraints_reject_unsavable_values() {
686        assert!(ItemDamageFunction::new(-1.0, 0.0, 1.0).is_err());
687        assert!(
688            BlocksAttacks::new(
689                -1.0,
690                1.0,
691                Vec::new(),
692                ItemDamageFunction::DEFAULT,
693                None,
694                None,
695                None,
696            )
697            .is_err()
698        );
699
700        for invalid in [-0.0, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
701            assert!(ItemDamageFunction::new(invalid, 0.0, 1.0).is_err());
702            assert!(
703                BlocksAttacks::new(
704                    invalid,
705                    1.0,
706                    Vec::new(),
707                    ItemDamageFunction::DEFAULT,
708                    None,
709                    None,
710                    None,
711                )
712                .is_err()
713            );
714        }
715        assert!(ItemDamageFunction::new(0.0, 0.0, 1.0).is_ok());
716
717        for invalid in [0.0, -0.0, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
718            assert!(DamageReduction::new(invalid, None, 0.0, 1.0).is_err());
719        }
720        assert!(DamageReduction::new(f32::MAX, None, 0.0, 1.0).is_ok());
721    }
722}