Skip to main content

steel_registry/data_components/components/
swing_animation.rs

1//! Vanilla `minecraft:swing_animation` item component.
2
3use std::io::{Cursor, 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
12/// Visual animation used when an item is swung.
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
14pub enum SwingAnimationType {
15    None,
16    #[default]
17    Whack,
18    Stab,
19}
20
21impl SwingAnimationType {
22    #[must_use]
23    pub const fn serialized_name(self) -> &'static str {
24        match self {
25            Self::None => "none",
26            Self::Whack => "whack",
27            Self::Stab => "stab",
28        }
29    }
30
31    const fn network_id(self) -> i32 {
32        match self {
33            Self::None => 0,
34            Self::Whack => 1,
35            Self::Stab => 2,
36        }
37    }
38
39    const fn from_network_id(id: i32) -> Self {
40        match id {
41            1 => Self::Whack,
42            2 => Self::Stab,
43            _ => Self::None,
44        }
45    }
46
47    /// resolves the case sensitive vanilla animation name
48    #[must_use]
49    pub const fn from_serialized_name(name: &str) -> Option<Self> {
50        match name {
51            "none" => Some(Self::None),
52            "whack" => Some(Self::Whack),
53            "stab" => Some(Self::Stab),
54            _ => None,
55        }
56    }
57}
58
59impl WriteTo for SwingAnimationType {
60    fn write(&self, writer: &mut impl Write) -> Result<()> {
61        VarInt(self.network_id()).write(writer)
62    }
63}
64
65impl ReadFrom for SwingAnimationType {
66    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
67        Ok(Self::from_network_id(VarInt::read(data)?.0))
68    }
69}
70
71impl HashComponent for SwingAnimationType {
72    fn hash_component(&self, hasher: &mut ComponentHasher) {
73        hasher.put_string(self.serialized_name());
74    }
75}
76
77/// Vanilla swing animation type and duration in ticks.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct SwingAnimation {
80    pub animation_type: SwingAnimationType,
81    pub duration: i32,
82}
83
84impl SwingAnimation {
85    pub const DEFAULT: Self = Self {
86        animation_type: SwingAnimationType::Whack,
87        duration: 6,
88    };
89
90    #[must_use]
91    pub const fn new(animation_type: SwingAnimationType, duration: i32) -> Self {
92        Self {
93            animation_type,
94            duration,
95        }
96    }
97}
98
99impl Default for SwingAnimation {
100    fn default() -> Self {
101        Self::DEFAULT
102    }
103}
104
105impl WriteTo for SwingAnimation {
106    fn write(&self, writer: &mut impl Write) -> Result<()> {
107        self.animation_type.write(writer)?;
108        VarInt(self.duration).write(writer)
109    }
110}
111
112impl ReadFrom for SwingAnimation {
113    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
114        Ok(Self {
115            animation_type: SwingAnimationType::read(data)?,
116            duration: VarInt::read(data)?.0,
117        })
118    }
119}
120
121impl ToNbtTag for SwingAnimation {
122    fn to_nbt_tag(self) -> NbtTag {
123        let mut compound = NbtCompound::new();
124        if self.animation_type != Self::DEFAULT.animation_type {
125            compound.insert("type", self.animation_type.serialized_name());
126        }
127        if self.duration != Self::DEFAULT.duration {
128            compound.insert("duration", self.duration);
129        }
130        NbtTag::Compound(compound)
131    }
132}
133
134impl FromNbtTag for SwingAnimation {
135    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
136        let compound = tag.compound()?;
137        let animation_type = match compound.get("type") {
138            Some(tag) => SwingAnimationType::from_serialized_name(&tag.string()?.to_str())?,
139            None => Self::DEFAULT.animation_type,
140        };
141        let duration = compound
142            .get("duration")
143            .map_or(Some(Self::DEFAULT.duration), |tag| tag.codec_i32())?;
144        (duration > 0).then_some(Self {
145            animation_type,
146            duration,
147        })
148    }
149}
150
151impl HashComponent for SwingAnimation {
152    fn hash_component(&self, hasher: &mut ComponentHasher) {
153        let mut entries = Vec::new();
154        if self.animation_type != Self::DEFAULT.animation_type {
155            push_hash_entry(&mut entries, "type", &self.animation_type);
156        }
157        if self.duration != Self::DEFAULT.duration {
158            push_hash_entry(&mut entries, "duration", &self.duration);
159        }
160        sort_map_entries(&mut entries);
161        hasher.start_map();
162        for entry in entries {
163            hasher.put_raw_bytes(&entry.key_bytes);
164            hasher.put_raw_bytes(&entry.value_bytes);
165        }
166        hasher.end_map();
167    }
168}
169
170fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
171    let mut key_hasher = ComponentHasher::new();
172    key_hasher.put_string(key);
173    let mut value_hasher = ComponentHasher::new();
174    value.hash_component(&mut value_hasher);
175    entries.push(HashEntry::new(key_hasher, value_hasher));
176}
177
178#[cfg(test)]
179mod tests {
180    use std::io::Cursor;
181
182    use simdnbt::FromNbtTag;
183    use simdnbt::borrow::read_tag;
184    use simdnbt::owned::{NbtCompound, NbtTag};
185    use steel_utils::codec::VarInt;
186    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
187
188    use super::{SwingAnimation, SwingAnimationType};
189
190    fn parse(tag: NbtTag) -> Option<SwingAnimation> {
191        let mut bytes = Vec::new();
192        tag.write(&mut bytes);
193        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
194        SwingAnimation::from_nbt_tag(borrowed.as_tag())
195    }
196
197    #[test]
198    fn empty_compound_uses_vanilla_default() {
199        assert_eq!(
200            parse(NbtTag::Compound(NbtCompound::new())),
201            Some(SwingAnimation::DEFAULT)
202        );
203    }
204
205    #[test]
206    fn persistent_duration_must_be_positive() {
207        for duration in [0, -1] {
208            let mut compound = NbtCompound::new();
209            compound.insert("duration", duration);
210            assert_eq!(parse(NbtTag::Compound(compound)), None);
211        }
212    }
213
214    #[test]
215    fn network_type_ids_fall_back_to_none() {
216        for id in [-1, 3, i32::MAX] {
217            let mut encoded = Vec::new();
218            VarInt(id).write(&mut encoded).expect("id should encode");
219            assert_eq!(
220                SwingAnimationType::read(&mut Cursor::new(encoded.as_slice()))
221                    .expect("animation type should decode"),
222                SwingAnimationType::None
223            );
224        }
225    }
226}