1use std::io::{Cursor, Error, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::Identifier;
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::nbt::NbtNumeric as _;
12use steel_utils::serial::{ReadFrom, WriteTo};
13
14use crate::damage_type::DamageTypeRef;
15use crate::sound_event::SoundEventHolder;
16use crate::{REGISTRY, RegistryEntry, RegistryExt};
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct DamageTypeComponent {
20 pub damage_type: DamageTypeRef,
21}
22
23impl DamageTypeComponent {
24 #[must_use]
25 pub const fn new(damage_type: DamageTypeRef) -> Self {
26 Self { damage_type }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub struct Weapon {
32 pub item_damage_per_attack: i32,
33 pub disable_blocking_for_seconds: f32,
34}
35
36impl Default for Weapon {
37 fn default() -> Self {
38 Self {
39 item_damage_per_attack: 1,
40 disable_blocking_for_seconds: 0.0,
41 }
42 }
43}
44
45impl WriteTo for DamageTypeComponent {
46 fn write(&self, writer: &mut impl Write) -> Result<()> {
47 let id = self.damage_type.try_id().ok_or_else(|| {
48 Error::other(format!("Unknown damage type: {}", self.damage_type.key))
49 })?;
50 let id = i32::try_from(id)
51 .map_err(|_| Error::other(format!("Damage type id out of protocol range: {id}")))?;
52 VarInt(id).write(writer)
53 }
54}
55
56impl ReadFrom for DamageTypeComponent {
57 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
58 let id = VarInt::read(data)?.0;
59 let id = usize::try_from(id)
60 .map_err(|_| Error::other(format!("Negative damage type id: {id}")))?;
61 let damage_type = REGISTRY
62 .damage_types
63 .by_id(id)
64 .ok_or_else(|| Error::other(format!("Unknown damage type id: {id}")))?;
65 Ok(Self { damage_type })
66 }
67}
68
69impl ToNbtTag for DamageTypeComponent {
70 fn to_nbt_tag(self) -> NbtTag {
71 self.damage_type.key.to_string().to_nbt_tag()
72 }
73}
74
75impl FromNbtTag for DamageTypeComponent {
76 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
77 let id = Identifier::from_str(&tag.string()?.to_str()).ok()?;
78 REGISTRY
79 .damage_types
80 .by_key(&id)
81 .map(|damage_type| Self { damage_type })
82 }
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub struct AttackRange {
87 pub min_reach: f32,
88 pub max_reach: f32,
89 pub min_creative_reach: f32,
90 pub max_creative_reach: f32,
91 pub hitbox_margin: f32,
92 pub mob_factor: f32,
93}
94
95impl Default for AttackRange {
96 fn default() -> Self {
97 Self {
98 min_reach: 0.0,
99 max_reach: 3.0,
100 min_creative_reach: 0.0,
101 max_creative_reach: 5.0,
102 hitbox_margin: 0.3,
103 mob_factor: 1.0,
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq)]
109pub struct PiercingWeapon {
110 pub deals_knockback: bool,
111 pub dismounts: bool,
112 pub sound: Option<SoundEventHolder>,
113 pub hit_sound: Option<SoundEventHolder>,
114}
115
116impl Default for PiercingWeapon {
117 fn default() -> Self {
118 Self {
119 deals_knockback: true,
120 dismounts: false,
121 sound: None,
122 hit_sound: None,
123 }
124 }
125}
126
127impl WriteTo for Weapon {
128 fn write(&self, writer: &mut impl Write) -> Result<()> {
129 VarInt(self.item_damage_per_attack).write(writer)?;
130 self.disable_blocking_for_seconds.write(writer)
131 }
132}
133
134impl ReadFrom for Weapon {
135 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
136 Ok(Self {
137 item_damage_per_attack: VarInt::read(data)?.0,
138 disable_blocking_for_seconds: f32::read(data)?,
139 })
140 }
141}
142
143impl WriteTo for AttackRange {
144 fn write(&self, writer: &mut impl Write) -> Result<()> {
145 self.min_reach.write(writer)?;
146 self.max_reach.write(writer)?;
147 self.min_creative_reach.write(writer)?;
148 self.max_creative_reach.write(writer)?;
149 self.hitbox_margin.write(writer)?;
150 self.mob_factor.write(writer)
151 }
152}
153
154impl ReadFrom for AttackRange {
155 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
156 Ok(Self {
157 min_reach: f32::read(data)?,
158 max_reach: f32::read(data)?,
159 min_creative_reach: f32::read(data)?,
160 max_creative_reach: f32::read(data)?,
161 hitbox_margin: f32::read(data)?,
162 mob_factor: f32::read(data)?,
163 })
164 }
165}
166
167impl WriteTo for PiercingWeapon {
168 fn write(&self, writer: &mut impl Write) -> Result<()> {
169 self.deals_knockback.write(writer)?;
170 self.dismounts.write(writer)?;
171 self.sound.write(writer)?;
172 self.hit_sound.write(writer)
173 }
174}
175
176impl ReadFrom for PiercingWeapon {
177 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
178 Ok(Self {
179 deals_knockback: bool::read(data)?,
180 dismounts: bool::read(data)?,
181 sound: Option::<SoundEventHolder>::read(data)?,
182 hit_sound: Option::<SoundEventHolder>::read(data)?,
183 })
184 }
185}
186
187impl ToNbtTag for Weapon {
188 fn to_nbt_tag(self) -> NbtTag {
189 let mut compound = NbtCompound::new();
190 if self.item_damage_per_attack != 1 {
191 compound.insert("item_damage_per_attack", self.item_damage_per_attack);
192 }
193 if self.disable_blocking_for_seconds.to_bits() != 0.0_f32.to_bits() {
194 compound.insert(
195 "disable_blocking_for_seconds",
196 self.disable_blocking_for_seconds,
197 );
198 }
199 NbtTag::Compound(compound)
200 }
201}
202
203impl FromNbtTag for Weapon {
204 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
205 let compound = tag.compound()?;
206 let item_damage_per_attack = match compound.get("item_damage_per_attack") {
207 Some(tag) => tag.codec_i32()?,
208 None => 1,
209 };
210 if item_damage_per_attack < 0 {
211 return None;
212 }
213 let disable_blocking_for_seconds = optional_ranged_f32(
214 compound.get("disable_blocking_for_seconds"),
215 0.0,
216 0.0,
217 f32::MAX,
218 )?;
219 Some(Self {
220 item_damage_per_attack,
221 disable_blocking_for_seconds,
222 })
223 }
224}
225
226impl ToNbtTag for AttackRange {
227 fn to_nbt_tag(self) -> NbtTag {
228 let default = Self::default();
229 let mut compound = NbtCompound::new();
230 if self.min_reach.to_bits() != default.min_reach.to_bits() {
231 compound.insert("min_reach", self.min_reach);
232 }
233 if self.max_reach.to_bits() != default.max_reach.to_bits() {
234 compound.insert("max_reach", self.max_reach);
235 }
236 if self.min_creative_reach.to_bits() != default.min_creative_reach.to_bits() {
237 compound.insert("min_creative_reach", self.min_creative_reach);
238 }
239 if self.max_creative_reach.to_bits() != default.max_creative_reach.to_bits() {
240 compound.insert("max_creative_reach", self.max_creative_reach);
241 }
242 if self.hitbox_margin.to_bits() != default.hitbox_margin.to_bits() {
243 compound.insert("hitbox_margin", self.hitbox_margin);
244 }
245 if self.mob_factor.to_bits() != default.mob_factor.to_bits() {
246 compound.insert("mob_factor", self.mob_factor);
247 }
248 NbtTag::Compound(compound)
249 }
250}
251
252impl FromNbtTag for AttackRange {
253 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
254 let compound = tag.compound()?;
255 let default = Self::default();
256 Some(Self {
257 min_reach: optional_ranged_f32(
258 compound.get("min_reach"),
259 default.min_reach,
260 0.0,
261 64.0,
262 )?,
263 max_reach: optional_ranged_f32(
264 compound.get("max_reach"),
265 default.max_reach,
266 0.0,
267 64.0,
268 )?,
269 min_creative_reach: optional_ranged_f32(
270 compound.get("min_creative_reach"),
271 default.min_creative_reach,
272 0.0,
273 64.0,
274 )?,
275 max_creative_reach: optional_ranged_f32(
276 compound.get("max_creative_reach"),
277 default.max_creative_reach,
278 0.0,
279 64.0,
280 )?,
281 hitbox_margin: optional_ranged_f32(
282 compound.get("hitbox_margin"),
283 default.hitbox_margin,
284 0.0,
285 1.0,
286 )?,
287 mob_factor: optional_ranged_f32(
288 compound.get("mob_factor"),
289 default.mob_factor,
290 0.0,
291 2.0,
292 )?,
293 })
294 }
295}
296
297impl ToNbtTag for PiercingWeapon {
298 fn to_nbt_tag(self) -> NbtTag {
299 let mut compound = NbtCompound::new();
300 if !self.deals_knockback {
301 compound.insert("deals_knockback", self.deals_knockback);
302 }
303 if self.dismounts {
304 compound.insert("dismounts", self.dismounts);
305 }
306 if let Some(sound) = self.sound {
307 compound.insert("sound", sound.to_nbt_tag());
308 }
309 if let Some(sound) = self.hit_sound {
310 compound.insert("hit_sound", sound.to_nbt_tag());
311 }
312 NbtTag::Compound(compound)
313 }
314}
315
316impl FromNbtTag for PiercingWeapon {
317 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
318 let compound = tag.compound()?;
319 let deals_knockback = match compound.get("deals_knockback") {
320 Some(tag) => tag.codec_bool()?,
321 None => true,
322 };
323 let dismounts = match compound.get("dismounts") {
324 Some(tag) => tag.codec_bool()?,
325 None => false,
326 };
327 let sound = match compound.get("sound") {
328 Some(tag) => Some(SoundEventHolder::from_nbt_tag(tag)?),
329 None => None,
330 };
331 let hit_sound = match compound.get("hit_sound") {
332 Some(tag) => Some(SoundEventHolder::from_nbt_tag(tag)?),
333 None => None,
334 };
335 Some(Self {
336 deals_knockback,
337 dismounts,
338 sound,
339 hit_sound,
340 })
341 }
342}
343
344fn optional_ranged_f32(
345 tag: Option<simdnbt::borrow::NbtTag<'_, '_>>,
346 default: f32,
347 min: f32,
348 max: f32,
349) -> Option<f32> {
350 let value = match tag {
351 Some(tag) => tag.codec_f32()?,
352 None => default,
353 };
354 (value.is_finite() && !value.is_sign_negative() && value >= min && value <= max)
355 .then_some(value)
356}
357
358impl HashComponent for Weapon {
359 fn hash_component(&self, hasher: &mut ComponentHasher) {
360 let mut entries = Vec::new();
361 if self.item_damage_per_attack != 1 {
362 push_hash_entry(
363 &mut entries,
364 "item_damage_per_attack",
365 &self.item_damage_per_attack,
366 );
367 }
368 if self.disable_blocking_for_seconds.to_bits() != 0.0_f32.to_bits() {
369 push_hash_entry(
370 &mut entries,
371 "disable_blocking_for_seconds",
372 &self.disable_blocking_for_seconds,
373 );
374 }
375 hash_entries(hasher, &mut entries);
376 }
377}
378
379impl HashComponent for DamageTypeComponent {
380 fn hash_component(&self, hasher: &mut ComponentHasher) {
381 hasher.put_string(&self.damage_type.key.to_string());
382 }
383}
384
385impl HashComponent for AttackRange {
386 fn hash_component(&self, hasher: &mut ComponentHasher) {
387 let default = Self::default();
388 let mut entries = Vec::new();
389 if self.min_reach.to_bits() != default.min_reach.to_bits() {
390 push_hash_entry(&mut entries, "min_reach", &self.min_reach);
391 }
392 if self.max_reach.to_bits() != default.max_reach.to_bits() {
393 push_hash_entry(&mut entries, "max_reach", &self.max_reach);
394 }
395 if self.min_creative_reach.to_bits() != default.min_creative_reach.to_bits() {
396 push_hash_entry(&mut entries, "min_creative_reach", &self.min_creative_reach);
397 }
398 if self.max_creative_reach.to_bits() != default.max_creative_reach.to_bits() {
399 push_hash_entry(&mut entries, "max_creative_reach", &self.max_creative_reach);
400 }
401 if self.hitbox_margin.to_bits() != default.hitbox_margin.to_bits() {
402 push_hash_entry(&mut entries, "hitbox_margin", &self.hitbox_margin);
403 }
404 if self.mob_factor.to_bits() != default.mob_factor.to_bits() {
405 push_hash_entry(&mut entries, "mob_factor", &self.mob_factor);
406 }
407 hash_entries(hasher, &mut entries);
408 }
409}
410
411impl HashComponent for PiercingWeapon {
412 fn hash_component(&self, hasher: &mut ComponentHasher) {
413 let mut entries = Vec::new();
414 if !self.deals_knockback {
415 push_hash_entry(&mut entries, "deals_knockback", &self.deals_knockback);
416 }
417 if self.dismounts {
418 push_hash_entry(&mut entries, "dismounts", &self.dismounts);
419 }
420 if let Some(sound) = &self.sound {
421 push_hash_entry(&mut entries, "sound", sound);
422 }
423 if let Some(sound) = &self.hit_sound {
424 push_hash_entry(&mut entries, "hit_sound", sound);
425 }
426 hash_entries(hasher, &mut entries);
427 }
428}
429
430fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
431 sort_map_entries(entries);
432 hasher.start_map();
433 for entry in entries {
434 hasher.put_raw_bytes(&entry.key_bytes);
435 hasher.put_raw_bytes(&entry.value_bytes);
436 }
437 hasher.end_map();
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
448#[cfg(test)]
449mod tests {
450 use std::io::Cursor;
451
452 use simdnbt::FromNbtTag;
453 use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
454 use simdnbt::owned::{NbtCompound, NbtTag};
455
456 use super::{AttackRange, Weapon};
457
458 fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
459 let mut bytes = Vec::new();
460 tag.write(&mut bytes);
461 let borrowed =
462 read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
463 visitor(borrowed.as_tag())
464 }
465
466 #[test]
467 fn combat_components_coerce_numbers_but_reject_malformed_present_fields() {
468 let mut weapon = NbtCompound::new();
469 weapon.insert("item_damage_per_attack", 2_i8);
470 weapon.insert("disable_blocking_for_seconds", 5.5_f64);
471 let weapon = with_borrowed_tag(NbtTag::Compound(weapon), Weapon::from_nbt_tag)
472 .expect("valid weapon should parse");
473 assert_eq!(weapon.item_damage_per_attack, 2);
474 assert_eq!(weapon.disable_blocking_for_seconds, 5.5);
475
476 let mut malformed = NbtCompound::new();
477 malformed.insert("item_damage_per_attack", "two");
478 assert!(with_borrowed_tag(NbtTag::Compound(malformed), Weapon::from_nbt_tag).is_none());
479
480 let mut out_of_range = NbtCompound::new();
481 out_of_range.insert("hitbox_margin", 1.5_f64);
482 assert!(
483 with_borrowed_tag(NbtTag::Compound(out_of_range), AttackRange::from_nbt_tag).is_none()
484 );
485 }
486}