steel_registry/data_components/components/
swing_animation.rs1use 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#[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 #[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#[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}