1use 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#[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#[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#[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}