Skip to main content

steel_registry/data_components/components/
kinetic_weapon.rs

1//! Vanilla `minecraft:kinetic_weapon` item component.
2
3use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, 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::sound_event::SoundEventHolder;
13
14/// Time and speed thresholds for one kinetic-weapon action.
15#[derive(Debug, Clone)]
16pub struct KineticWeaponCondition {
17    max_duration_ticks: i32,
18    min_speed: f32,
19    min_relative_speed: f32,
20}
21
22impl PartialEq for KineticWeaponCondition {
23    fn eq(&self, other: &Self) -> bool {
24        self.max_duration_ticks == other.max_duration_ticks
25            && java_float_equals(self.min_speed, other.min_speed)
26            && java_float_equals(self.min_relative_speed, other.min_relative_speed)
27    }
28}
29
30impl KineticWeaponCondition {
31    pub fn new(max_duration_ticks: i32, min_speed: f32, min_relative_speed: f32) -> Result<Self> {
32        if max_duration_ticks < 0 {
33            return Err(Error::other(
34                "Kinetic weapon condition duration must be non-negative",
35            ));
36        }
37        Ok(Self {
38            max_duration_ticks,
39            min_speed,
40            min_relative_speed,
41        })
42    }
43
44    pub(crate) const fn from_extracted(
45        max_duration_ticks: i32,
46        min_speed: f32,
47        min_relative_speed: f32,
48    ) -> Self {
49        assert!(
50            max_duration_ticks >= 0,
51            "extracted kinetic weapon duration must be non-negative"
52        );
53        Self {
54            max_duration_ticks,
55            min_speed,
56            min_relative_speed,
57        }
58    }
59
60    #[must_use]
61    pub const fn max_duration_ticks(&self) -> i32 {
62        self.max_duration_ticks
63    }
64
65    #[must_use]
66    pub const fn min_speed(&self) -> f32 {
67        self.min_speed
68    }
69
70    #[must_use]
71    pub const fn min_relative_speed(&self) -> f32 {
72        self.min_relative_speed
73    }
74
75    fn to_nbt_tag_ref(&self) -> NbtTag {
76        let mut compound = NbtCompound::new();
77        compound.insert("max_duration_ticks", self.max_duration_ticks);
78        if self.min_speed.to_bits() != 0.0_f32.to_bits() {
79            compound.insert("min_speed", self.min_speed);
80        }
81        if self.min_relative_speed.to_bits() != 0.0_f32.to_bits() {
82            compound.insert("min_relative_speed", self.min_relative_speed);
83        }
84        NbtTag::Compound(compound)
85    }
86
87    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
88        let compound = tag.compound()?;
89        let max_duration_ticks = compound.get("max_duration_ticks")?.codec_i32()?;
90        let min_speed = optional_owned_f32(compound.get("min_speed"), 0.0)?;
91        let min_relative_speed = optional_owned_f32(compound.get("min_relative_speed"), 0.0)?;
92        Self::new(max_duration_ticks, min_speed, min_relative_speed).ok()
93    }
94}
95
96impl WriteTo for KineticWeaponCondition {
97    fn write(&self, writer: &mut impl Write) -> Result<()> {
98        VarInt(self.max_duration_ticks).write(writer)?;
99        self.min_speed.write(writer)?;
100        self.min_relative_speed.write(writer)
101    }
102}
103
104impl ReadFrom for KineticWeaponCondition {
105    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
106        Self::new(VarInt::read(data)?.0, f32::read(data)?, f32::read(data)?)
107    }
108}
109
110impl HashComponent for KineticWeaponCondition {
111    fn hash_component(&self, hasher: &mut ComponentHasher) {
112        let mut entries = Vec::with_capacity(3);
113        push_hash_entry(&mut entries, "max_duration_ticks", &self.max_duration_ticks);
114        if self.min_speed.to_bits() != 0.0_f32.to_bits() {
115            push_hash_entry(&mut entries, "min_speed", &self.min_speed);
116        }
117        if self.min_relative_speed.to_bits() != 0.0_f32.to_bits() {
118            push_hash_entry(&mut entries, "min_relative_speed", &self.min_relative_speed);
119        }
120        hash_entries(hasher, &mut entries);
121    }
122}
123
124/// Data controlling a spear-like continuous kinetic attack.
125#[derive(Debug, Clone)]
126pub struct KineticWeapon {
127    contact_cooldown_ticks: i32,
128    delay_ticks: i32,
129    dismount_conditions: Option<KineticWeaponCondition>,
130    knockback_conditions: Option<KineticWeaponCondition>,
131    damage_conditions: Option<KineticWeaponCondition>,
132    forward_movement: f32,
133    damage_multiplier: f32,
134    sound: Option<SoundEventHolder>,
135    hit_sound: Option<SoundEventHolder>,
136}
137
138impl PartialEq for KineticWeapon {
139    fn eq(&self, other: &Self) -> bool {
140        self.contact_cooldown_ticks == other.contact_cooldown_ticks
141            && self.delay_ticks == other.delay_ticks
142            && self.dismount_conditions == other.dismount_conditions
143            && self.knockback_conditions == other.knockback_conditions
144            && self.damage_conditions == other.damage_conditions
145            && java_float_equals(self.forward_movement, other.forward_movement)
146            && java_float_equals(self.damage_multiplier, other.damage_multiplier)
147            && self.sound == other.sound
148            && self.hit_sound == other.hit_sound
149    }
150}
151
152impl KineticWeapon {
153    #[expect(clippy::too_many_arguments, reason = "mirrors Vanilla's record fields")]
154    pub fn new(
155        contact_cooldown_ticks: i32,
156        delay_ticks: i32,
157        dismount_conditions: Option<KineticWeaponCondition>,
158        knockback_conditions: Option<KineticWeaponCondition>,
159        damage_conditions: Option<KineticWeaponCondition>,
160        forward_movement: f32,
161        damage_multiplier: f32,
162        sound: Option<SoundEventHolder>,
163        hit_sound: Option<SoundEventHolder>,
164    ) -> Result<Self> {
165        if contact_cooldown_ticks < 0 || delay_ticks < 0 {
166            return Err(Error::other(
167                "Kinetic weapon cooldown and delay must be non-negative",
168            ));
169        }
170        Ok(Self {
171            contact_cooldown_ticks,
172            delay_ticks,
173            dismount_conditions,
174            knockback_conditions,
175            damage_conditions,
176            forward_movement,
177            damage_multiplier,
178            sound,
179            hit_sound,
180        })
181    }
182
183    #[expect(clippy::too_many_arguments, reason = "mirrors Vanilla's record fields")]
184    pub(crate) fn from_extracted(
185        contact_cooldown_ticks: i32,
186        delay_ticks: i32,
187        dismount_conditions: Option<KineticWeaponCondition>,
188        knockback_conditions: Option<KineticWeaponCondition>,
189        damage_conditions: Option<KineticWeaponCondition>,
190        forward_movement: f32,
191        damage_multiplier: f32,
192        sound: Option<SoundEventHolder>,
193        hit_sound: Option<SoundEventHolder>,
194    ) -> Self {
195        assert!(
196            contact_cooldown_ticks >= 0 && delay_ticks >= 0,
197            "extracted kinetic weapon durations must be non-negative"
198        );
199        Self {
200            contact_cooldown_ticks,
201            delay_ticks,
202            dismount_conditions,
203            knockback_conditions,
204            damage_conditions,
205            forward_movement,
206            damage_multiplier,
207            sound,
208            hit_sound,
209        }
210    }
211
212    #[must_use]
213    pub const fn contact_cooldown_ticks(&self) -> i32 {
214        self.contact_cooldown_ticks
215    }
216
217    #[must_use]
218    pub const fn delay_ticks(&self) -> i32 {
219        self.delay_ticks
220    }
221
222    #[must_use]
223    pub const fn dismount_conditions(&self) -> Option<&KineticWeaponCondition> {
224        self.dismount_conditions.as_ref()
225    }
226
227    #[must_use]
228    pub const fn knockback_conditions(&self) -> Option<&KineticWeaponCondition> {
229        self.knockback_conditions.as_ref()
230    }
231
232    #[must_use]
233    pub const fn damage_conditions(&self) -> Option<&KineticWeaponCondition> {
234        self.damage_conditions.as_ref()
235    }
236
237    #[must_use]
238    pub const fn forward_movement(&self) -> f32 {
239        self.forward_movement
240    }
241
242    #[must_use]
243    pub const fn damage_multiplier(&self) -> f32 {
244        self.damage_multiplier
245    }
246
247    #[must_use]
248    pub const fn sound(&self) -> Option<&SoundEventHolder> {
249        self.sound.as_ref()
250    }
251
252    #[must_use]
253    pub const fn hit_sound(&self) -> Option<&SoundEventHolder> {
254        self.hit_sound.as_ref()
255    }
256}
257
258impl WriteTo for KineticWeapon {
259    fn write(&self, writer: &mut impl Write) -> Result<()> {
260        VarInt(self.contact_cooldown_ticks).write(writer)?;
261        VarInt(self.delay_ticks).write(writer)?;
262        self.dismount_conditions.write(writer)?;
263        self.knockback_conditions.write(writer)?;
264        self.damage_conditions.write(writer)?;
265        self.forward_movement.write(writer)?;
266        self.damage_multiplier.write(writer)?;
267        self.sound.write(writer)?;
268        self.hit_sound.write(writer)
269    }
270}
271
272impl ReadFrom for KineticWeapon {
273    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
274        Self::new(
275            VarInt::read(data)?.0,
276            VarInt::read(data)?.0,
277            Option::<KineticWeaponCondition>::read(data)?,
278            Option::<KineticWeaponCondition>::read(data)?,
279            Option::<KineticWeaponCondition>::read(data)?,
280            f32::read(data)?,
281            f32::read(data)?,
282            Option::<SoundEventHolder>::read(data)?,
283            Option::<SoundEventHolder>::read(data)?,
284        )
285    }
286}
287
288impl ToNbtTag for KineticWeapon {
289    fn to_nbt_tag(self) -> NbtTag {
290        let mut compound = NbtCompound::new();
291        if self.contact_cooldown_ticks != 10 {
292            compound.insert("contact_cooldown_ticks", self.contact_cooldown_ticks);
293        }
294        if self.delay_ticks != 0 {
295            compound.insert("delay_ticks", self.delay_ticks);
296        }
297        if let Some(condition) = self.dismount_conditions {
298            compound.insert("dismount_conditions", condition.to_nbt_tag_ref());
299        }
300        if let Some(condition) = self.knockback_conditions {
301            compound.insert("knockback_conditions", condition.to_nbt_tag_ref());
302        }
303        if let Some(condition) = self.damage_conditions {
304            compound.insert("damage_conditions", condition.to_nbt_tag_ref());
305        }
306        if self.forward_movement.to_bits() != 0.0_f32.to_bits() {
307            compound.insert("forward_movement", self.forward_movement);
308        }
309        if self.damage_multiplier.to_bits() != 1.0_f32.to_bits() {
310            compound.insert("damage_multiplier", self.damage_multiplier);
311        }
312        if let Some(sound) = self.sound {
313            compound.insert("sound", sound.to_nbt_tag());
314        }
315        if let Some(sound) = self.hit_sound {
316            compound.insert("hit_sound", sound.to_nbt_tag());
317        }
318        NbtTag::Compound(compound)
319    }
320}
321
322impl FromNbtTag for KineticWeapon {
323    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
324        let compound = tag.compound()?;
325        let contact_cooldown_ticks = optional_i32(compound.get("contact_cooldown_ticks"), 10)?;
326        let delay_ticks = optional_i32(compound.get("delay_ticks"), 0)?;
327        let dismount_conditions = optional_condition(compound.get("dismount_conditions"))?;
328        let knockback_conditions = optional_condition(compound.get("knockback_conditions"))?;
329        let damage_conditions = optional_condition(compound.get("damage_conditions"))?;
330        let forward_movement = optional_f32(compound.get("forward_movement"), 0.0)?;
331        let damage_multiplier = optional_f32(compound.get("damage_multiplier"), 1.0)?;
332        let sound = optional_sound(compound.get("sound"))?;
333        let hit_sound = optional_sound(compound.get("hit_sound"))?;
334        Self::new(
335            contact_cooldown_ticks,
336            delay_ticks,
337            dismount_conditions,
338            knockback_conditions,
339            damage_conditions,
340            forward_movement,
341            damage_multiplier,
342            sound,
343            hit_sound,
344        )
345        .ok()
346    }
347}
348
349impl HashComponent for KineticWeapon {
350    fn hash_component(&self, hasher: &mut ComponentHasher) {
351        let mut entries = Vec::with_capacity(9);
352        if self.contact_cooldown_ticks != 10 {
353            push_hash_entry(
354                &mut entries,
355                "contact_cooldown_ticks",
356                &self.contact_cooldown_ticks,
357            );
358        }
359        if self.delay_ticks != 0 {
360            push_hash_entry(&mut entries, "delay_ticks", &self.delay_ticks);
361        }
362        if let Some(condition) = &self.dismount_conditions {
363            push_hash_entry(&mut entries, "dismount_conditions", condition);
364        }
365        if let Some(condition) = &self.knockback_conditions {
366            push_hash_entry(&mut entries, "knockback_conditions", condition);
367        }
368        if let Some(condition) = &self.damage_conditions {
369            push_hash_entry(&mut entries, "damage_conditions", condition);
370        }
371        if self.forward_movement.to_bits() != 0.0_f32.to_bits() {
372            push_hash_entry(&mut entries, "forward_movement", &self.forward_movement);
373        }
374        if self.damage_multiplier.to_bits() != 1.0_f32.to_bits() {
375            push_hash_entry(&mut entries, "damage_multiplier", &self.damage_multiplier);
376        }
377        if let Some(sound) = &self.sound {
378            push_hash_entry(&mut entries, "sound", sound);
379        }
380        if let Some(sound) = &self.hit_sound {
381            push_hash_entry(&mut entries, "hit_sound", sound);
382        }
383        hash_entries(hasher, &mut entries);
384    }
385}
386
387fn optional_i32(tag: Option<simdnbt::borrow::NbtTag<'_, '_>>, default: i32) -> Option<i32> {
388    match tag {
389        Some(tag) => tag.codec_i32(),
390        None => Some(default),
391    }
392}
393
394fn optional_f32<T: steel_utils::nbt::NbtNumeric>(tag: Option<T>, default: f32) -> Option<f32> {
395    match tag {
396        Some(tag) => tag.codec_f32(),
397        None => Some(default),
398    }
399}
400
401fn optional_owned_f32(tag: Option<&NbtTag>, default: f32) -> Option<f32> {
402    match tag {
403        Some(tag) => tag.codec_f32(),
404        None => Some(default),
405    }
406}
407
408const fn java_float_equals(left: f32, right: f32) -> bool {
409    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
410}
411
412#[expect(
413    clippy::option_option,
414    reason = "the outer option reports codec failure while the inner option represents an absent field"
415)]
416fn optional_condition(
417    tag: Option<simdnbt::borrow::NbtTag<'_, '_>>,
418) -> Option<Option<KineticWeaponCondition>> {
419    match tag {
420        Some(tag) => Some(Some(KineticWeaponCondition::from_owned_nbt(
421            &tag.to_owned(),
422        )?)),
423        None => Some(None),
424    }
425}
426
427#[expect(
428    clippy::option_option,
429    reason = "the outer option reports codec failure while the inner option represents an absent field"
430)]
431fn optional_sound(
432    tag: Option<simdnbt::borrow::NbtTag<'_, '_>>,
433) -> Option<Option<SoundEventHolder>> {
434    match tag {
435        Some(tag) => Some(Some(SoundEventHolder::from_nbt_tag(tag)?)),
436        None => Some(None),
437    }
438}
439
440fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
441    let mut key_hasher = ComponentHasher::new();
442    key_hasher.put_string(key);
443    let mut value_hasher = ComponentHasher::new();
444    value.hash_component(&mut value_hasher);
445    entries.push(HashEntry::new(key_hasher, value_hasher));
446}
447
448fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
449    sort_map_entries(entries);
450    hasher.start_map();
451    for entry in entries {
452        hasher.put_raw_bytes(&entry.key_bytes);
453        hasher.put_raw_bytes(&entry.value_bytes);
454    }
455    hasher.end_map();
456}
457
458#[cfg(test)]
459mod tests {
460    use std::io::Cursor;
461
462    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
463    use steel_utils::hash::HashComponent as _;
464    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
465
466    use super::{KineticWeapon, KineticWeaponCondition};
467    use crate::data_components::vanilla_components::KINETIC_WEAPON;
468    use crate::init_vanilla_registry;
469    use crate::{REGISTRY, RegistryExt};
470
471    fn parse(tag: simdnbt::owned::NbtTag) -> Option<KineticWeapon> {
472        let mut bytes = Vec::new();
473        tag.write(&mut bytes);
474        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
475        KineticWeapon::from_nbt_tag(borrowed.as_tag())
476    }
477
478    #[test]
479    fn kinetic_weapon_round_trips_both_codecs_and_hashes_record_shape() {
480        let value = KineticWeapon::new(
481            10,
482            8,
483            Some(KineticWeaponCondition::new(50, 9.0, 0.0).expect("valid condition")),
484            None,
485            Some(KineticWeaponCondition::new(175, 0.0, 4.6).expect("valid condition")),
486            0.38,
487            1.2,
488            None,
489            None,
490        )
491        .expect("valid kinetic weapon");
492        let nbt = value.clone().to_nbt_tag();
493        assert_eq!(parse(nbt.clone()), Some(value.clone()));
494        assert_eq!(value.compute_hash(), nbt.compute_hash());
495
496        let mut network = Vec::new();
497        value.write(&mut network).expect("weapon should encode");
498        assert_eq!(
499            KineticWeapon::read(&mut Cursor::new(network.as_slice()))
500                .expect("weapon should decode"),
501            value
502        );
503    }
504
505    #[test]
506    fn negative_persistent_durations_are_rejected() {
507        assert!(KineticWeaponCondition::new(-1, 0.0, 0.0).is_err());
508        assert!(KineticWeapon::new(-1, 0, None, None, None, 0.0, 1.0, None, None).is_err());
509    }
510
511    #[test]
512    fn equality_uses_java_record_float_semantics() {
513        let first_nan = f32::from_bits(0x7fc0_0001);
514        let second_nan = f32::from_bits(0x7fc0_0002);
515        assert_eq!(
516            KineticWeaponCondition::new(1, first_nan, 0.0).expect("valid condition"),
517            KineticWeaponCondition::new(1, second_nan, 0.0).expect("valid condition")
518        );
519        assert_ne!(
520            KineticWeaponCondition::new(1, 0.0, 0.0).expect("valid condition"),
521            KineticWeaponCondition::new(1, -0.0, 0.0).expect("valid condition")
522        );
523
524        assert_eq!(
525            KineticWeapon::new(10, 0, None, None, None, first_nan, 1.0, None, None)
526                .expect("valid weapon"),
527            KineticWeapon::new(10, 0, None, None, None, second_nan, 1.0, None, None)
528                .expect("valid weapon")
529        );
530        assert_ne!(
531            KineticWeapon::new(10, 0, None, None, None, 0.0, 1.0, None, None)
532                .expect("valid weapon"),
533            KineticWeapon::new(10, 0, None, None, None, -0.0, 1.0, None, None)
534                .expect("valid weapon")
535        );
536    }
537
538    #[test]
539    fn extracted_netherite_spear_keeps_kinetic_thresholds_and_sounds() {
540        init_vanilla_registry();
541        let spear = REGISTRY
542            .items
543            .by_key(&steel_utils::Identifier::vanilla_static("netherite_spear"))
544            .expect("netherite spear should be registered");
545        let kinetic = spear
546            .components
547            .get(KINETIC_WEAPON)
548            .expect("netherite spear should have a kinetic weapon component");
549        assert_eq!(kinetic.delay_ticks(), 8);
550        assert_eq!(kinetic.forward_movement(), 0.38);
551        assert_eq!(kinetic.damage_multiplier(), 1.2);
552        assert_eq!(
553            kinetic
554                .damage_conditions()
555                .expect("damage condition")
556                .min_relative_speed(),
557            4.6
558        );
559        assert_eq!(
560            kinetic
561                .sound()
562                .and_then(crate::sound_event::SoundEventHolder::registry_ref)
563                .map(|sound| &sound.key),
564            Some(&steel_utils::Identifier::vanilla_static("item.spear.use"))
565        );
566        assert_eq!(
567            kinetic
568                .hit_sound()
569                .and_then(crate::sound_event::SoundEventHolder::registry_ref)
570                .map(|sound| &sound.key),
571            Some(&steel_utils::Identifier::vanilla_static("item.spear.hit"))
572        );
573    }
574}