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