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