steel_registry/data_components/components/
suspicious_stew.rs1use std::io::{Cursor, Error, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, NbtList, 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::serial::{ReadFrom, WriteTo};
12
13use crate::mob_effect::MobEffectRef;
14use crate::{REGISTRY, RegistryEntry, RegistryExt};
15
16#[derive(Debug, Clone)]
18pub struct SuspiciousStewEffect {
19 effect: MobEffectRef,
20 duration: i32,
21}
22
23impl SuspiciousStewEffect {
24 pub const DEFAULT_DURATION: i32 = 160;
25
26 #[must_use]
27 pub const fn new(effect: MobEffectRef, duration: i32) -> Self {
28 Self { effect, duration }
29 }
30
31 #[must_use]
32 pub const fn effect(&self) -> MobEffectRef {
33 self.effect
34 }
35
36 #[must_use]
37 pub const fn duration(&self) -> i32 {
38 self.duration
39 }
40
41 fn to_nbt_compound(&self) -> NbtCompound {
42 let mut compound = NbtCompound::new();
43 compound.insert("id", self.effect.key.to_string());
44 if self.duration != Self::DEFAULT_DURATION {
45 compound.insert("duration", self.duration);
46 }
47 compound
48 }
49
50 fn from_nbt_compound(compound: &NbtCompound) -> Option<Self> {
51 let id = Identifier::from_str(&compound.get("id")?.string()?.to_string()).ok()?;
52 let effect = REGISTRY.mob_effects.by_key(&id)?;
53 let duration = compound
54 .get("duration")
55 .and_then(steel_utils::nbt::NbtNumeric::codec_i32)
56 .unwrap_or(Self::DEFAULT_DURATION);
57 Some(Self::new(effect, duration))
58 }
59}
60
61impl PartialEq for SuspiciousStewEffect {
62 fn eq(&self, other: &Self) -> bool {
63 self.effect.key == other.effect.key && self.duration == other.duration
64 }
65}
66
67impl WriteTo for SuspiciousStewEffect {
68 fn write(&self, writer: &mut impl Write) -> Result<()> {
69 let id = self
70 .effect
71 .try_id()
72 .ok_or_else(|| Error::other(format!("Unknown mob effect: {}", self.effect.key)))?;
73 let id = i32::try_from(id)
74 .map_err(|_| Error::other(format!("Mob effect id out of range: {id}")))?;
75 VarInt(id).write(writer)?;
76 VarInt(self.duration).write(writer)
77 }
78}
79
80impl ReadFrom for SuspiciousStewEffect {
81 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
82 let id = VarInt::read(data)?.0;
83 let id = usize::try_from(id)
84 .map_err(|_| Error::other(format!("Negative mob effect id: {id}")))?;
85 let effect = REGISTRY
86 .mob_effects
87 .by_id(id)
88 .ok_or_else(|| Error::other(format!("Unknown mob effect id: {id}")))?;
89 Ok(Self::new(effect, VarInt::read(data)?.0))
90 }
91}
92
93impl HashComponent for SuspiciousStewEffect {
94 fn hash_component(&self, hasher: &mut ComponentHasher) {
95 let mut entries = Vec::with_capacity(2);
96 push_hash_entry(&mut entries, "id", &self.effect.key);
97 if self.duration != Self::DEFAULT_DURATION {
98 push_hash_entry(&mut entries, "duration", &self.duration);
99 }
100 hash_entries(hasher, &mut entries);
101 }
102}
103
104#[derive(Debug, Default, Clone, PartialEq)]
106pub struct SuspiciousStewEffects {
107 effects: Vec<SuspiciousStewEffect>,
108}
109
110impl SuspiciousStewEffects {
111 #[must_use]
112 pub const fn empty() -> Self {
113 Self {
114 effects: Vec::new(),
115 }
116 }
117
118 #[must_use]
119 pub const fn new(effects: Vec<SuspiciousStewEffect>) -> Self {
120 Self { effects }
121 }
122
123 #[must_use]
124 pub fn effects(&self) -> &[SuspiciousStewEffect] {
125 &self.effects
126 }
127}
128
129impl WriteTo for SuspiciousStewEffects {
130 fn write(&self, writer: &mut impl Write) -> Result<()> {
131 write_count(self.effects.len(), writer)?;
132 for effect in &self.effects {
133 effect.write(writer)?;
134 }
135 Ok(())
136 }
137}
138
139impl ReadFrom for SuspiciousStewEffects {
140 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
141 let count = read_count(data)?;
142 let mut effects = Vec::with_capacity(count.min(65_536));
143 for _ in 0..count {
144 effects.push(SuspiciousStewEffect::read(data)?);
145 }
146 Ok(Self::new(effects))
147 }
148}
149
150impl ToNbtTag for SuspiciousStewEffects {
151 fn to_nbt_tag(self) -> NbtTag {
152 if self.effects.is_empty() {
153 NbtTag::List(NbtList::Empty)
154 } else {
155 NbtTag::List(NbtList::Compound(
156 self.effects
157 .iter()
158 .map(SuspiciousStewEffect::to_nbt_compound)
159 .collect(),
160 ))
161 }
162 }
163}
164
165impl FromNbtTag for SuspiciousStewEffects {
166 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
167 let effects = tag
168 .list()?
169 .to_owned()
170 .as_nbt_tags()
171 .iter()
172 .map(|tag| SuspiciousStewEffect::from_nbt_compound(tag.compound()?))
173 .collect::<Option<Vec<_>>>()?;
174 Some(Self::new(effects))
175 }
176}
177
178impl HashComponent for SuspiciousStewEffects {
179 fn hash_component(&self, hasher: &mut ComponentHasher) {
180 hasher.start_list();
181 for effect in &self.effects {
182 hasher.put_component_hash(effect);
183 }
184 hasher.end_list();
185 }
186}
187
188fn write_count(count: usize, writer: &mut impl Write) -> Result<()> {
189 let count = i32::try_from(count).map_err(|_| Error::other("Effect list is too large"))?;
190 VarInt(count).write(writer)
191}
192
193fn read_count(data: &mut Cursor<&[u8]>) -> Result<usize> {
194 let count = VarInt::read(data)?.0;
195 usize::try_from(count).map_err(|_| Error::other(format!("Negative effect count: {count}")))
196}
197
198fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
199 let mut key_hasher = ComponentHasher::new();
200 key_hasher.put_string(key);
201 let mut value_hasher = ComponentHasher::new();
202 value.hash_component(&mut value_hasher);
203 entries.push(HashEntry::new(key_hasher, value_hasher));
204}
205
206fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
207 sort_map_entries(entries);
208 hasher.start_map();
209 for entry in entries {
210 hasher.put_raw_bytes(&entry.key_bytes);
211 hasher.put_raw_bytes(&entry.value_bytes);
212 }
213 hasher.end_map();
214}
215
216#[cfg(test)]
217mod tests {
218 use std::io::Cursor;
219
220 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
221 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
222
223 use super::{SuspiciousStewEffect, SuspiciousStewEffects};
224 use crate::data_components::vanilla_components::SUSPICIOUS_STEW_EFFECTS;
225 use crate::init_vanilla_registry;
226 use crate::{REGISTRY, RegistryExt};
227
228 #[test]
229 fn stew_effects_round_trip_and_malformed_duration_uses_lenient_default() {
230 init_vanilla_registry();
231 let night_vision = REGISTRY
232 .mob_effects
233 .by_key(&steel_utils::Identifier::vanilla_static("night_vision"))
234 .expect("night vision should be registered");
235 let value = SuspiciousStewEffects::new(vec![SuspiciousStewEffect::new(
236 night_vision,
237 SuspiciousStewEffect::DEFAULT_DURATION,
238 )]);
239 let nbt = value.clone().to_nbt_tag();
240 let mut bytes = Vec::new();
241 nbt.write(&mut bytes);
242 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice()))
243 .expect("stew NBT should parse");
244 assert_eq!(
245 SuspiciousStewEffects::from_nbt_tag(borrowed.as_tag()),
246 Some(value.clone())
247 );
248 let mut network = Vec::new();
249 value.write(&mut network).expect("effects should encode");
250 assert_eq!(
251 SuspiciousStewEffects::read(&mut Cursor::new(network.as_slice()))
252 .expect("effects should decode"),
253 value
254 );
255 }
256
257 #[test]
258 fn extracted_suspicious_stew_has_an_empty_effect_list() {
259 init_vanilla_registry();
260 let stew = REGISTRY
261 .items
262 .by_key(&steel_utils::Identifier::vanilla_static("suspicious_stew"))
263 .expect("suspicious stew should be registered");
264 assert_eq!(
265 stew.components.get(SUSPICIOUS_STEW_EFFECTS),
266 Some(SuspiciousStewEffects::empty())
267 );
268 }
269}