Skip to main content

steel_registry/mob_effect/
instance.rs

1//! Vanilla mob-effect instance codec model.
2
3use std::io::{Cursor, Error, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, 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::nbt::NbtNumeric as _;
12use steel_utils::serial::{ReadFrom, WriteTo};
13
14use crate::mob_effect::MobEffectRef;
15use crate::{REGISTRY, RegistryEntry, RegistryExt};
16
17const MAX_EFFECT_DEPTH: usize = 512;
18
19/// One status-effect instance, including Vanilla's hidden fallback chain.
20#[derive(Debug, Clone)]
21pub struct MobEffectInstance {
22    effect: MobEffectRef,
23    duration: i32,
24    amplifier: i32,
25    ambient: bool,
26    show_particles: bool,
27    show_icon: bool,
28    hidden_effect: Option<Box<MobEffectInstanceDetails>>,
29}
30
31/// Recursive fields shared by a visible effect and its hidden fallbacks.
32#[derive(Debug, Clone)]
33pub struct MobEffectInstanceDetails {
34    amplifier: i32,
35    duration: i32,
36    ambient: bool,
37    show_particles: bool,
38    show_icon: bool,
39    hidden_effect: Option<Box<Self>>,
40}
41
42impl MobEffectInstance {
43    #[must_use]
44    pub fn new(
45        effect: MobEffectRef,
46        duration: i32,
47        amplifier: i32,
48        ambient: bool,
49        show_particles: bool,
50        show_icon: bool,
51        hidden_effect: Option<MobEffectInstanceDetails>,
52    ) -> Self {
53        Self {
54            effect,
55            duration,
56            amplifier: amplifier.clamp(0, 255),
57            ambient,
58            show_particles,
59            show_icon,
60            hidden_effect: hidden_effect.map(Box::new),
61        }
62    }
63
64    #[must_use]
65    pub fn simple(effect: MobEffectRef, duration: i32, amplifier: i32) -> Self {
66        Self::new(effect, duration, amplifier, false, true, true, None)
67    }
68
69    #[must_use]
70    pub const fn effect(&self) -> MobEffectRef {
71        self.effect
72    }
73
74    #[must_use]
75    pub const fn duration(&self) -> i32 {
76        self.duration
77    }
78
79    #[must_use]
80    pub const fn amplifier(&self) -> i32 {
81        self.amplifier
82    }
83
84    #[must_use]
85    pub const fn ambient(&self) -> bool {
86        self.ambient
87    }
88
89    #[must_use]
90    pub const fn show_particles(&self) -> bool {
91        self.show_particles
92    }
93
94    #[must_use]
95    pub const fn show_icon(&self) -> bool {
96        self.show_icon
97    }
98
99    #[must_use]
100    pub fn hidden_effect(&self) -> Option<&MobEffectInstanceDetails> {
101        self.hidden_effect.as_deref()
102    }
103
104    fn details(&self) -> MobEffectInstanceDetails {
105        MobEffectInstanceDetails {
106            amplifier: self.amplifier,
107            duration: self.duration,
108            ambient: self.ambient,
109            show_particles: self.show_particles,
110            show_icon: self.show_icon,
111            hidden_effect: self.hidden_effect.clone(),
112        }
113    }
114
115    fn from_details(effect: MobEffectRef, details: MobEffectInstanceDetails) -> Self {
116        Self::new(
117            effect,
118            details.duration,
119            details.amplifier,
120            details.ambient,
121            details.show_particles,
122            details.show_icon,
123            details.hidden_effect.map(|hidden| *hidden),
124        )
125    }
126
127    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
128        let mut compound = self.details().to_nbt_compound();
129        compound.insert("id", self.effect.key.to_string());
130        NbtTag::Compound(compound)
131    }
132
133    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
134        let compound = tag.compound()?;
135        let id = Identifier::from_str(&compound.get("id")?.string()?.to_string()).ok()?;
136        let effect = REGISTRY.mob_effects.by_key(&id)?;
137        let details = MobEffectInstanceDetails::from_owned_compound(compound, 0)?;
138        Some(Self::from_details(effect, details))
139    }
140}
141
142impl PartialEq for MobEffectInstance {
143    fn eq(&self, other: &Self) -> bool {
144        self.effect.key == other.effect.key
145            && self.duration == other.duration
146            && self.amplifier == other.amplifier
147            && self.ambient == other.ambient
148            && self.show_particles == other.show_particles
149            && self.show_icon == other.show_icon
150    }
151}
152
153impl MobEffectInstanceDetails {
154    #[must_use]
155    pub fn new(
156        amplifier: i32,
157        duration: i32,
158        ambient: bool,
159        show_particles: bool,
160        show_icon: bool,
161        hidden_effect: Option<Self>,
162    ) -> Self {
163        Self {
164            amplifier: amplifier.clamp(0, 255),
165            duration,
166            ambient,
167            show_particles,
168            show_icon,
169            hidden_effect: hidden_effect.map(Box::new),
170        }
171    }
172
173    fn to_nbt_compound(&self) -> NbtCompound {
174        let mut compound = NbtCompound::new();
175        if self.amplifier != 0 {
176            compound.insert("amplifier", self.amplifier as u8 as i8);
177        }
178        if self.duration != 0 {
179            compound.insert("duration", self.duration);
180        }
181        if self.ambient {
182            compound.insert("ambient", true);
183        }
184        if !self.show_particles {
185            compound.insert("show_particles", false);
186        }
187        compound.insert("show_icon", self.show_icon);
188        if let Some(hidden) = &self.hidden_effect {
189            compound.insert("hidden_effect", NbtTag::Compound(hidden.to_nbt_compound()));
190        }
191        compound
192    }
193
194    fn from_owned_compound(compound: &NbtCompound, depth: usize) -> Option<Self> {
195        if depth >= MAX_EFFECT_DEPTH {
196            return None;
197        }
198        let amplifier = match compound.get("amplifier") {
199            Some(tag) => i32::from(tag.codec_i32()? as i8 as u8),
200            None => 0,
201        };
202        let duration = optional_i32(compound.get("duration"), 0)?;
203        let ambient = optional_bool(compound.get("ambient"), false)?;
204        let show_particles = optional_bool(compound.get("show_particles"), true)?;
205        let show_icon = match compound.get("show_icon") {
206            Some(tag) => tag.codec_bool()?,
207            None => show_particles,
208        };
209        let hidden_effect = match compound.get("hidden_effect") {
210            Some(tag) => Some(Self::from_owned_compound(tag.compound()?, depth + 1)?),
211            None => None,
212        };
213        Some(Self::new(
214            amplifier,
215            duration,
216            ambient,
217            show_particles,
218            show_icon,
219            hidden_effect,
220        ))
221    }
222}
223
224impl PartialEq for MobEffectInstanceDetails {
225    fn eq(&self, other: &Self) -> bool {
226        self.amplifier == other.amplifier
227            && self.duration == other.duration
228            && self.ambient == other.ambient
229            && self.show_particles == other.show_particles
230            && self.show_icon == other.show_icon
231            && self.hidden_effect == other.hidden_effect
232    }
233}
234
235impl WriteTo for MobEffectInstance {
236    fn write(&self, writer: &mut impl Write) -> Result<()> {
237        let id = self
238            .effect
239            .try_id()
240            .ok_or_else(|| Error::other(format!("Unknown mob effect: {}", self.effect.key)))?;
241        let id = i32::try_from(id)
242            .map_err(|_| Error::other(format!("Mob effect id out of range: {id}")))?;
243        VarInt(id).write(writer)?;
244        write_details(&self.details(), writer, 0)
245    }
246}
247
248impl ReadFrom for MobEffectInstance {
249    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
250        let id = VarInt::read(data)?.0;
251        let id = usize::try_from(id)
252            .map_err(|_| Error::other(format!("Negative mob effect id: {id}")))?;
253        let effect = REGISTRY
254            .mob_effects
255            .by_id(id)
256            .ok_or_else(|| Error::other(format!("Unknown mob effect id: {id}")))?;
257        Ok(Self::from_details(effect, read_details(data, 0)?))
258    }
259}
260
261impl ToNbtTag for MobEffectInstance {
262    fn to_nbt_tag(self) -> NbtTag {
263        self.to_nbt_tag_ref()
264    }
265}
266
267impl FromNbtTag for MobEffectInstance {
268    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
269        Self::from_owned_nbt(&tag.to_owned())
270    }
271}
272
273impl HashComponent for MobEffectInstance {
274    fn hash_component(&self, hasher: &mut ComponentHasher) {
275        let mut entries = details_hash_entries(&self.details());
276        push_hash_entry(&mut entries, "id", &self.effect.key);
277        hash_entries(hasher, &mut entries);
278    }
279}
280
281impl HashComponent for MobEffectInstanceDetails {
282    fn hash_component(&self, hasher: &mut ComponentHasher) {
283        let mut entries = details_hash_entries(self);
284        hash_entries(hasher, &mut entries);
285    }
286}
287
288fn write_details(
289    details: &MobEffectInstanceDetails,
290    writer: &mut impl Write,
291    depth: usize,
292) -> Result<()> {
293    if depth >= MAX_EFFECT_DEPTH {
294        return Err(Error::other("Mob effect hidden chain is too deep"));
295    }
296    VarInt(details.amplifier).write(writer)?;
297    VarInt(details.duration).write(writer)?;
298    details.ambient.write(writer)?;
299    details.show_particles.write(writer)?;
300    details.show_icon.write(writer)?;
301    details.hidden_effect.is_some().write(writer)?;
302    if let Some(hidden) = &details.hidden_effect {
303        write_details(hidden, writer, depth + 1)?;
304    }
305    Ok(())
306}
307
308fn read_details(data: &mut Cursor<&[u8]>, depth: usize) -> Result<MobEffectInstanceDetails> {
309    if depth >= MAX_EFFECT_DEPTH {
310        return Err(Error::other("Mob effect hidden chain is too deep"));
311    }
312    let amplifier = VarInt::read(data)?.0;
313    let duration = VarInt::read(data)?.0;
314    let ambient = bool::read(data)?;
315    let show_particles = bool::read(data)?;
316    let show_icon = bool::read(data)?;
317    let hidden_effect = if bool::read(data)? {
318        Some(read_details(data, depth + 1)?)
319    } else {
320        None
321    };
322    Ok(MobEffectInstanceDetails::new(
323        amplifier,
324        duration,
325        ambient,
326        show_particles,
327        show_icon,
328        hidden_effect,
329    ))
330}
331
332fn details_hash_entries(details: &MobEffectInstanceDetails) -> Vec<HashEntry> {
333    let mut entries = Vec::with_capacity(6);
334    if details.amplifier != 0 {
335        push_hash_entry(&mut entries, "amplifier", &(details.amplifier as u8 as i8));
336    }
337    if details.duration != 0 {
338        push_hash_entry(&mut entries, "duration", &details.duration);
339    }
340    if details.ambient {
341        push_hash_entry(&mut entries, "ambient", &true);
342    }
343    if !details.show_particles {
344        push_hash_entry(&mut entries, "show_particles", &false);
345    }
346    push_hash_entry(&mut entries, "show_icon", &details.show_icon);
347    if let Some(hidden) = &details.hidden_effect {
348        push_hash_entry(&mut entries, "hidden_effect", hidden.as_ref());
349    }
350    entries
351}
352
353fn optional_i32(tag: Option<&NbtTag>, default: i32) -> Option<i32> {
354    match tag {
355        Some(tag) => tag.codec_i32(),
356        None => Some(default),
357    }
358}
359
360fn optional_bool(tag: Option<&NbtTag>, default: bool) -> Option<bool> {
361    match tag {
362        Some(tag) => tag.codec_bool(),
363        None => Some(default),
364    }
365}
366
367fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
368    let mut key_hasher = ComponentHasher::new();
369    key_hasher.put_string(key);
370    let mut value_hasher = ComponentHasher::new();
371    value.hash_component(&mut value_hasher);
372    entries.push(HashEntry::new(key_hasher, value_hasher));
373}
374
375fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
376    sort_map_entries(entries);
377    hasher.start_map();
378    for entry in entries {
379        hasher.put_raw_bytes(&entry.key_bytes);
380        hasher.put_raw_bytes(&entry.value_bytes);
381    }
382    hasher.end_map();
383}
384
385#[cfg(test)]
386mod tests {
387    use std::io::Cursor;
388
389    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
390    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
391
392    use super::{MobEffectInstance, MobEffectInstanceDetails};
393    use crate::init_vanilla_registry;
394    use crate::{REGISTRY, RegistryExt};
395
396    #[test]
397    fn effect_instances_round_trip_recursive_details_and_clamp_amplifier() {
398        init_vanilla_registry();
399        let speed = REGISTRY
400            .mob_effects
401            .by_key(&steel_utils::Identifier::vanilla_static("speed"))
402            .expect("speed should be registered");
403        let value = MobEffectInstance::new(
404            speed,
405            200,
406            300,
407            false,
408            true,
409            false,
410            Some(MobEffectInstanceDetails::new(
411                1, 400, true, false, false, None,
412            )),
413        );
414        assert_eq!(value.amplifier(), 255);
415        let nbt = value.clone().to_nbt_tag();
416        let mut bytes = Vec::new();
417        nbt.write(&mut bytes);
418        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice()))
419            .expect("effect NBT should parse");
420        assert_eq!(
421            MobEffectInstance::from_nbt_tag(borrowed.as_tag()),
422            Some(value.clone())
423        );
424
425        let mut network = Vec::new();
426        value.write(&mut network).expect("effect should encode");
427        assert_eq!(
428            MobEffectInstance::read(&mut Cursor::new(network.as_slice()))
429                .expect("effect should decode"),
430            value
431        );
432    }
433
434    #[test]
435    fn effect_instance_equality_ignores_hidden_effects_like_vanilla() {
436        init_vanilla_registry();
437        let speed = REGISTRY
438            .mob_effects
439            .by_key(&steel_utils::Identifier::vanilla_static("speed"))
440            .expect("speed should be registered");
441        let without_hidden = MobEffectInstance::simple(speed, 200, 1);
442        let with_hidden = MobEffectInstance::new(
443            speed,
444            200,
445            1,
446            false,
447            true,
448            true,
449            Some(MobEffectInstanceDetails::new(
450                2, 400, false, true, true, None,
451            )),
452        );
453
454        assert_eq!(with_hidden, without_hidden);
455    }
456}