steel_registry/data_components/components/
potion_contents.rs1use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtList, 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::{PrefixedRead as _, PrefixedWrite as _, ReadFrom, WriteTo};
11
12use crate::RegistryReference;
13use crate::mob_effect_instance::MobEffectInstance;
14use crate::potion::Potion;
15
16#[derive(Debug, Default, Clone, PartialEq)]
18pub struct PotionContents {
19 potion: Option<RegistryReference<Potion>>,
20 custom_color: Option<i32>,
21 custom_effects: Vec<MobEffectInstance>,
22 custom_name: Option<String>,
23}
24
25impl PotionContents {
26 const MAX_NETWORK_STRING_LENGTH: usize = 32_767;
27
28 #[must_use]
29 pub const fn empty() -> Self {
30 Self {
31 potion: None,
32 custom_color: None,
33 custom_effects: Vec::new(),
34 custom_name: None,
35 }
36 }
37
38 #[must_use]
39 pub const fn new(
40 potion: Option<RegistryReference<Potion>>,
41 custom_color: Option<i32>,
42 custom_effects: Vec<MobEffectInstance>,
43 custom_name: Option<String>,
44 ) -> Self {
45 Self {
46 potion,
47 custom_color,
48 custom_effects,
49 custom_name,
50 }
51 }
52
53 #[must_use]
54 pub const fn potion(&self) -> Option<RegistryReference<Potion>> {
55 self.potion
56 }
57
58 #[must_use]
59 pub const fn custom_color(&self) -> Option<i32> {
60 self.custom_color
61 }
62
63 #[must_use]
64 pub fn custom_effects(&self) -> &[MobEffectInstance] {
65 &self.custom_effects
66 }
67
68 #[must_use]
69 pub fn custom_name(&self) -> Option<&str> {
70 self.custom_name.as_deref()
71 }
72
73 #[must_use]
74 pub fn is(&self, potion: &Potion) -> bool {
75 self.potion.is_some_and(|p| p.value().key == potion.key) && self.custom_effects.is_empty()
76 }
77
78 #[must_use]
81 pub fn all_effects(&self) -> Vec<MobEffectInstance> {
82 let mut effects = Vec::with_capacity(self.custom_effects.len() + 1);
83 if let Some(potion) = self.potion {
84 effects.extend(potion.value().effects.iter().map(|effect| {
85 MobEffectInstance::simple(effect.effect, effect.duration, effect.amplifier)
86 }));
87 }
88 effects.extend(self.custom_effects.iter().cloned());
89 effects
90 }
91
92 fn to_nbt_tag_ref(&self) -> NbtTag {
93 let mut compound = NbtCompound::new();
94 if let Some(potion) = self.potion {
95 compound.insert("potion", potion.to_nbt_tag());
96 }
97 if let Some(custom_color) = self.custom_color {
98 compound.insert("custom_color", custom_color);
99 }
100 if !self.custom_effects.is_empty() {
101 compound.insert(
102 "custom_effects",
103 NbtList::Compound(
104 self.custom_effects
105 .iter()
106 .map(|effect| match effect.to_nbt_tag_ref() {
107 NbtTag::Compound(compound) => compound,
108 _ => unreachable!("mob effect codec always produces a compound"),
109 })
110 .collect(),
111 ),
112 );
113 }
114 if let Some(custom_name) = &self.custom_name {
115 compound.insert("custom_name", custom_name.clone());
116 }
117 NbtTag::Compound(compound)
118 }
119
120 fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
121 if tag.string().is_some() {
122 return registry_reference_from_owned_nbt(tag)
123 .map(|potion| Self::new(Some(potion), None, Vec::new(), None));
124 }
125
126 let compound = tag.compound()?;
127 let potion = match compound.get("potion") {
128 Some(tag) => Some(registry_reference_from_owned_nbt(tag)?),
129 None => None,
130 };
131 let custom_color = match compound.get("custom_color") {
132 Some(tag) => Some(tag.codec_i32()?),
133 None => None,
134 };
135 let custom_effects = match compound.get("custom_effects") {
136 Some(tag) => tag
137 .list()?
138 .as_nbt_tags()
139 .iter()
140 .map(MobEffectInstance::from_owned_nbt)
141 .collect::<Option<Vec<_>>>()?,
142 None => Vec::new(),
143 };
144 let custom_name = match compound.get("custom_name") {
145 Some(tag) => Some(tag.string()?.to_string()),
146 None => None,
147 };
148 Some(Self::new(potion, custom_color, custom_effects, custom_name))
149 }
150}
151
152impl WriteTo for PotionContents {
153 fn write(&self, writer: &mut impl Write) -> Result<()> {
154 self.potion.is_some().write(writer)?;
155 if let Some(potion) = self.potion {
156 potion.write(writer)?;
157 }
158 self.custom_color.is_some().write(writer)?;
159 if let Some(custom_color) = self.custom_color {
160 custom_color.write(writer)?;
161 }
162 write_count(self.custom_effects.len(), writer)?;
163 for effect in &self.custom_effects {
164 effect.write(writer)?;
165 }
166 self.custom_name.is_some().write(writer)?;
167 if let Some(custom_name) = &self.custom_name {
168 write_network_string(custom_name, writer)?;
169 }
170 Ok(())
171 }
172}
173
174impl ReadFrom for PotionContents {
175 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
176 let potion = if bool::read(data)? {
177 Some(RegistryReference::read(data)?)
178 } else {
179 None
180 };
181 let custom_color = if bool::read(data)? {
182 Some(i32::read(data)?)
183 } else {
184 None
185 };
186 let count = read_count(data)?;
187 let mut custom_effects = Vec::with_capacity(count.min(65_536));
188 for _ in 0..count {
189 custom_effects.push(MobEffectInstance::read(data)?);
190 }
191 let custom_name = if bool::read(data)? {
192 Some(read_network_string(data)?)
193 } else {
194 None
195 };
196 Ok(Self::new(potion, custom_color, custom_effects, custom_name))
197 }
198}
199
200impl ToNbtTag for PotionContents {
201 fn to_nbt_tag(self) -> NbtTag {
202 self.to_nbt_tag_ref()
203 }
204}
205
206impl FromNbtTag for PotionContents {
207 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
208 Self::from_owned_nbt(&tag.to_owned())
209 }
210}
211
212impl HashComponent for PotionContents {
213 fn hash_component(&self, hasher: &mut ComponentHasher) {
214 let mut entries = Vec::with_capacity(4);
215 if let Some(potion) = &self.potion {
216 push_hash_entry(&mut entries, "potion", potion);
217 }
218 if let Some(custom_color) = self.custom_color {
219 push_hash_entry(&mut entries, "custom_color", &custom_color);
220 }
221 if !self.custom_effects.is_empty() {
222 push_hash_entry(
223 &mut entries,
224 "custom_effects",
225 &MobEffectList(&self.custom_effects),
226 );
227 }
228 if let Some(custom_name) = &self.custom_name {
229 push_hash_entry(&mut entries, "custom_name", custom_name);
230 }
231 sort_map_entries(&mut entries);
232 hasher.start_map();
233 for entry in entries {
234 hasher.put_raw_bytes(&entry.key_bytes);
235 hasher.put_raw_bytes(&entry.value_bytes);
236 }
237 hasher.end_map();
238 }
239}
240
241struct MobEffectList<'a>(&'a [MobEffectInstance]);
242
243impl HashComponent for MobEffectList<'_> {
244 fn hash_component(&self, hasher: &mut ComponentHasher) {
245 hasher.start_list();
246 for effect in self.0 {
247 hasher.put_component_hash(effect);
248 }
249 hasher.end_list();
250 }
251}
252
253fn registry_reference_from_owned_nbt(tag: &NbtTag) -> Option<RegistryReference<Potion>> {
254 let key = tag.string()?.to_string().parse().ok()?;
255 <Potion as crate::RegistryReferenceEntry>::reference_by_key(&key).map(RegistryReference::new)
256}
257
258fn write_count(count: usize, writer: &mut impl Write) -> Result<()> {
259 let count = i32::try_from(count).map_err(|_| Error::other("Effect list is too large"))?;
260 VarInt(count).write(writer)
261}
262
263fn read_count(data: &mut Cursor<&[u8]>) -> Result<usize> {
264 let count = VarInt::read(data)?.0;
265 usize::try_from(count).map_err(|_| Error::other(format!("Negative effect count: {count}")))
266}
267
268fn write_network_string(value: &str, writer: &mut impl Write) -> Result<()> {
269 if value.encode_utf16().count() > PotionContents::MAX_NETWORK_STRING_LENGTH
270 || value.len() > PotionContents::MAX_NETWORK_STRING_LENGTH * 3
271 {
272 return Err(Error::other("Potion custom name exceeds the network limit"));
273 }
274 value.write_prefixed::<VarInt>(writer)
275}
276
277fn read_network_string(data: &mut Cursor<&[u8]>) -> Result<String> {
278 let value =
279 String::read_prefixed_bound::<VarInt>(data, PotionContents::MAX_NETWORK_STRING_LENGTH * 3)?;
280 if value.encode_utf16().count() > PotionContents::MAX_NETWORK_STRING_LENGTH {
281 return Err(Error::other("Potion custom name exceeds the network limit"));
282 }
283 Ok(value)
284}
285
286fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
287 let mut key_hasher = ComponentHasher::new();
288 key_hasher.put_string(key);
289 let mut value_hasher = ComponentHasher::new();
290 value.hash_component(&mut value_hasher);
291 entries.push(HashEntry::new(key_hasher, value_hasher));
292}
293
294#[cfg(test)]
295mod tests {
296 use std::io::Cursor;
297
298 use simdnbt::owned::NbtTag;
299 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
300 use steel_utils::hash::HashComponent as _;
301 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
302
303 use super::PotionContents;
304 use crate::data_components::vanilla_components::POTION_CONTENTS;
305 use crate::init_vanilla_registry;
306 use crate::{REGISTRY, RegistryExt, RegistryReference, vanilla_mob_effects, vanilla_potions};
307
308 fn parse(tag: NbtTag) -> Option<PotionContents> {
309 let mut bytes = Vec::new();
310 tag.write(&mut bytes);
311 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
312 PotionContents::from_nbt_tag(borrowed.as_tag())
313 }
314
315 #[test]
316 fn full_and_alternative_potion_codecs_round_trip() {
317 init_vanilla_registry();
318 let value = PotionContents::new(
319 Some(RegistryReference::new(&vanilla_potions::SWIFTNESS)),
320 Some(0x12_34_56),
321 vec![crate::MobEffectInstance::simple(
322 vanilla_mob_effects::LUCK,
323 200,
324 1,
325 )],
326 Some("custom".to_owned()),
327 );
328 let nbt = value.clone().to_nbt_tag();
329 assert_eq!(parse(nbt.clone()), Some(value.clone()));
330 assert_ne!(value.compute_hash(), nbt.compute_hash());
333
334 let mut network = Vec::new();
335 value
336 .write(&mut network)
337 .expect("potion contents should encode");
338 assert_eq!(
339 PotionContents::read(&mut Cursor::new(network.as_slice()))
340 .expect("potion contents should decode"),
341 value
342 );
343
344 assert_eq!(
345 parse(NbtTag::String("minecraft:water".into())),
346 Some(PotionContents::new(
347 Some(RegistryReference::new(&vanilla_potions::WATER)),
348 None,
349 Vec::new(),
350 None,
351 ))
352 );
353 }
354
355 #[test]
356 fn all_effects_combines_base_potion_then_custom_effects() {
357 init_vanilla_registry();
358 let custom = crate::MobEffectInstance::simple(vanilla_mob_effects::LUCK, 200, 1);
359 let contents = PotionContents::new(
360 Some(RegistryReference::new(&vanilla_potions::POISON)),
361 None,
362 vec![custom.clone()],
363 None,
364 );
365
366 let base_effects = vanilla_potions::POISON.effects;
367 let expected: Vec<_> = base_effects
368 .iter()
369 .map(|effect| {
370 crate::MobEffectInstance::simple(effect.effect, effect.duration, effect.amplifier)
371 })
372 .chain(std::iter::once(custom))
373 .collect();
374
375 assert_eq!(contents.all_effects(), expected);
376 }
377
378 #[test]
379 fn all_effects_is_empty_without_a_base_potion_or_custom_effects() {
380 init_vanilla_registry();
381 assert_eq!(PotionContents::empty().all_effects(), Vec::new());
382 }
383
384 #[test]
385 fn extracted_potion_items_have_empty_contents() {
386 init_vanilla_registry();
387 for name in [
388 "potion",
389 "splash_potion",
390 "tipped_arrow",
391 "lingering_potion",
392 ] {
393 let item = REGISTRY
394 .items
395 .by_key(&steel_utils::Identifier::vanilla(name.to_owned()))
396 .expect("potion item should be registered");
397 assert_eq!(
398 item.components.get(POTION_CONTENTS),
399 Some(PotionContents::empty())
400 );
401 }
402 }
403}