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