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/// Duration sentinel for effects that never expire
20pub const INFINITE_EFFECT_DURATION: i32 = -1;
21
22/// One status-effect instance, including Vanilla's hidden fallback chain.
23#[derive(Debug, Clone)]
24pub struct MobEffectInstance {
25    effect: MobEffectRef,
26    duration: i32,
27    amplifier: i32,
28    ambient: bool,
29    show_particles: bool,
30    show_icon: bool,
31    hidden_effect: Option<Box<MobEffectInstanceDetails>>,
32}
33
34/// Recursive fields shared by a visible effect and its hidden fallbacks.
35#[derive(Debug, Clone)]
36pub struct MobEffectInstanceDetails {
37    amplifier: i32,
38    duration: i32,
39    ambient: bool,
40    show_particles: bool,
41    show_icon: bool,
42    hidden_effect: Option<Box<Self>>,
43}
44
45impl MobEffectInstance {
46    #[must_use]
47    pub fn new(
48        effect: MobEffectRef,
49        duration: i32,
50        amplifier: i32,
51        ambient: bool,
52        show_particles: bool,
53        show_icon: bool,
54        hidden_effect: Option<MobEffectInstanceDetails>,
55    ) -> Self {
56        Self {
57            effect,
58            duration,
59            amplifier: amplifier.clamp(0, 255),
60            ambient,
61            show_particles,
62            show_icon,
63            hidden_effect: hidden_effect.map(Box::new),
64        }
65    }
66
67    #[must_use]
68    pub fn simple(effect: MobEffectRef, duration: i32, amplifier: i32) -> Self {
69        Self::new(effect, duration, amplifier, false, true, true, None)
70    }
71
72    #[must_use]
73    pub const fn effect(&self) -> MobEffectRef {
74        self.effect
75    }
76
77    #[must_use]
78    pub const fn duration(&self) -> i32 {
79        self.duration
80    }
81
82    #[must_use]
83    pub const fn amplifier(&self) -> i32 {
84        self.amplifier
85    }
86
87    /// Whether this effect never expires.
88    #[must_use]
89    pub const fn is_infinite_duration(&self) -> bool {
90        self.duration == INFINITE_EFFECT_DURATION
91    }
92
93    /// Applies `mapper` to this effect's duration, leaving the infinite and zero sentinels alone.
94    #[must_use]
95    pub fn map_duration(&self, mapper: impl FnOnce(i32) -> i32) -> i32 {
96        if self.is_infinite_duration() || self.duration == 0 {
97            self.duration
98        } else {
99            mapper(self.duration)
100        }
101    }
102
103    /// Returns a copy of this effect with its duration scaled by `scale`,
104    /// never rounding a finite duration below 1 tick. The infinite and zero
105    /// sentinels are left alone.
106    #[must_use]
107    pub fn with_scaled_duration(&self, scale: f32) -> Self {
108        Self {
109            duration: self
110                .map_duration(|duration| ((duration as f32 * scale).floor() as i32).max(1)),
111            ..self.clone()
112        }
113    }
114
115    #[must_use]
116    pub const fn ambient(&self) -> bool {
117        self.ambient
118    }
119
120    #[must_use]
121    pub const fn show_particles(&self) -> bool {
122        self.show_particles
123    }
124
125    #[must_use]
126    pub const fn show_icon(&self) -> bool {
127        self.show_icon
128    }
129
130    #[must_use]
131    pub fn hidden_effect(&self) -> Option<&MobEffectInstanceDetails> {
132        self.hidden_effect.as_deref()
133    }
134
135    fn details(&self) -> MobEffectInstanceDetails {
136        MobEffectInstanceDetails {
137            amplifier: self.amplifier,
138            duration: self.duration,
139            ambient: self.ambient,
140            show_particles: self.show_particles,
141            show_icon: self.show_icon,
142            hidden_effect: self.hidden_effect.clone(),
143        }
144    }
145
146    fn from_details(effect: MobEffectRef, details: MobEffectInstanceDetails) -> Self {
147        Self::new(
148            effect,
149            details.duration,
150            details.amplifier,
151            details.ambient,
152            details.show_particles,
153            details.show_icon,
154            details.hidden_effect.map(|hidden| *hidden),
155        )
156    }
157
158    pub(crate) fn to_nbt_tag_ref(&self) -> NbtTag {
159        let mut compound = self.details().to_nbt_compound();
160        compound.insert("id", self.effect.key.to_string());
161        NbtTag::Compound(compound)
162    }
163
164    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
165        let compound = tag.compound()?;
166        let id = Identifier::from_str(&compound.get("id")?.string()?.to_string()).ok()?;
167        let effect = REGISTRY.mob_effects.by_key(&id)?;
168        let details = MobEffectInstanceDetails::from_owned_compound(compound, 0)?;
169        Some(Self::from_details(effect, details))
170    }
171}
172
173impl PartialEq for MobEffectInstance {
174    fn eq(&self, other: &Self) -> bool {
175        self.effect.key == other.effect.key
176            && self.duration == other.duration
177            && self.amplifier == other.amplifier
178            && self.ambient == other.ambient
179            && self.show_particles == other.show_particles
180            && self.show_icon == other.show_icon
181    }
182}
183
184impl MobEffectInstanceDetails {
185    #[must_use]
186    pub fn new(
187        amplifier: i32,
188        duration: i32,
189        ambient: bool,
190        show_particles: bool,
191        show_icon: bool,
192        hidden_effect: Option<Self>,
193    ) -> Self {
194        Self {
195            amplifier: amplifier.clamp(0, 255),
196            duration,
197            ambient,
198            show_particles,
199            show_icon,
200            hidden_effect: hidden_effect.map(Box::new),
201        }
202    }
203
204    fn to_nbt_compound(&self) -> NbtCompound {
205        let mut compound = NbtCompound::new();
206        if self.amplifier != 0 {
207            compound.insert("amplifier", self.amplifier as u8 as i8);
208        }
209        if self.duration != 0 {
210            compound.insert("duration", self.duration);
211        }
212        if self.ambient {
213            compound.insert("ambient", true);
214        }
215        if !self.show_particles {
216            compound.insert("show_particles", false);
217        }
218        compound.insert("show_icon", self.show_icon);
219        if let Some(hidden) = &self.hidden_effect {
220            compound.insert("hidden_effect", NbtTag::Compound(hidden.to_nbt_compound()));
221        }
222        compound
223    }
224
225    fn from_owned_compound(compound: &NbtCompound, depth: usize) -> Option<Self> {
226        if depth >= MAX_EFFECT_DEPTH {
227            return None;
228        }
229        let amplifier = match compound.get("amplifier") {
230            Some(tag) => i32::from(tag.codec_i32()? as i8 as u8),
231            None => 0,
232        };
233        let duration = optional_i32(compound.get("duration"), 0)?;
234        let ambient = optional_bool(compound.get("ambient"), false)?;
235        let show_particles = optional_bool(compound.get("show_particles"), true)?;
236        let show_icon = match compound.get("show_icon") {
237            Some(tag) => tag.codec_bool()?,
238            None => show_particles,
239        };
240        let hidden_effect = match compound.get("hidden_effect") {
241            Some(tag) => Some(Self::from_owned_compound(tag.compound()?, depth + 1)?),
242            None => None,
243        };
244        Some(Self::new(
245            amplifier,
246            duration,
247            ambient,
248            show_particles,
249            show_icon,
250            hidden_effect,
251        ))
252    }
253}
254
255impl PartialEq for MobEffectInstanceDetails {
256    fn eq(&self, other: &Self) -> bool {
257        self.amplifier == other.amplifier
258            && self.duration == other.duration
259            && self.ambient == other.ambient
260            && self.show_particles == other.show_particles
261            && self.show_icon == other.show_icon
262            && self.hidden_effect == other.hidden_effect
263    }
264}
265
266impl WriteTo for MobEffectInstance {
267    fn write(&self, writer: &mut impl Write) -> Result<()> {
268        let id = self
269            .effect
270            .try_id()
271            .ok_or_else(|| Error::other(format!("Unknown mob effect: {}", self.effect.key)))?;
272        let id = i32::try_from(id)
273            .map_err(|_| Error::other(format!("Mob effect id out of range: {id}")))?;
274        VarInt(id).write(writer)?;
275        write_details(&self.details(), writer, 0)
276    }
277}
278
279impl ReadFrom for MobEffectInstance {
280    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
281        let id = VarInt::read(data)?.0;
282        let id = usize::try_from(id)
283            .map_err(|_| Error::other(format!("Negative mob effect id: {id}")))?;
284        let effect = REGISTRY
285            .mob_effects
286            .by_id(id)
287            .ok_or_else(|| Error::other(format!("Unknown mob effect id: {id}")))?;
288        Ok(Self::from_details(effect, read_details(data, 0)?))
289    }
290}
291
292impl ToNbtTag for MobEffectInstance {
293    fn to_nbt_tag(self) -> NbtTag {
294        self.to_nbt_tag_ref()
295    }
296}
297
298impl FromNbtTag for MobEffectInstance {
299    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
300        Self::from_owned_nbt(&tag.to_owned())
301    }
302}
303
304impl HashComponent for MobEffectInstance {
305    fn hash_component(&self, hasher: &mut ComponentHasher) {
306        let mut entries = details_hash_entries(&self.details());
307        push_hash_entry(&mut entries, "id", &self.effect.key);
308        hash_entries(hasher, &mut entries);
309    }
310}
311
312impl HashComponent for MobEffectInstanceDetails {
313    fn hash_component(&self, hasher: &mut ComponentHasher) {
314        let mut entries = details_hash_entries(self);
315        hash_entries(hasher, &mut entries);
316    }
317}
318
319fn write_details(
320    details: &MobEffectInstanceDetails,
321    writer: &mut impl Write,
322    depth: usize,
323) -> Result<()> {
324    if depth >= MAX_EFFECT_DEPTH {
325        return Err(Error::other("Mob effect hidden chain is too deep"));
326    }
327    VarInt(details.amplifier).write(writer)?;
328    VarInt(details.duration).write(writer)?;
329    details.ambient.write(writer)?;
330    details.show_particles.write(writer)?;
331    details.show_icon.write(writer)?;
332    details.hidden_effect.is_some().write(writer)?;
333    if let Some(hidden) = &details.hidden_effect {
334        write_details(hidden, writer, depth + 1)?;
335    }
336    Ok(())
337}
338
339fn read_details(data: &mut Cursor<&[u8]>, depth: usize) -> Result<MobEffectInstanceDetails> {
340    if depth >= MAX_EFFECT_DEPTH {
341        return Err(Error::other("Mob effect hidden chain is too deep"));
342    }
343    let amplifier = VarInt::read(data)?.0;
344    let duration = VarInt::read(data)?.0;
345    let ambient = bool::read(data)?;
346    let show_particles = bool::read(data)?;
347    let show_icon = bool::read(data)?;
348    let hidden_effect = if bool::read(data)? {
349        Some(read_details(data, depth + 1)?)
350    } else {
351        None
352    };
353    Ok(MobEffectInstanceDetails::new(
354        amplifier,
355        duration,
356        ambient,
357        show_particles,
358        show_icon,
359        hidden_effect,
360    ))
361}
362
363fn details_hash_entries(details: &MobEffectInstanceDetails) -> Vec<HashEntry> {
364    let mut entries = Vec::with_capacity(6);
365    if details.amplifier != 0 {
366        push_hash_entry(&mut entries, "amplifier", &(details.amplifier as u8 as i8));
367    }
368    if details.duration != 0 {
369        push_hash_entry(&mut entries, "duration", &details.duration);
370    }
371    if details.ambient {
372        push_hash_entry(&mut entries, "ambient", &true);
373    }
374    if !details.show_particles {
375        push_hash_entry(&mut entries, "show_particles", &false);
376    }
377    push_hash_entry(&mut entries, "show_icon", &details.show_icon);
378    if let Some(hidden) = &details.hidden_effect {
379        push_hash_entry(&mut entries, "hidden_effect", hidden.as_ref());
380    }
381    entries
382}
383
384fn optional_i32(tag: Option<&NbtTag>, default: i32) -> Option<i32> {
385    match tag {
386        Some(tag) => tag.codec_i32(),
387        None => Some(default),
388    }
389}
390
391fn optional_bool(tag: Option<&NbtTag>, default: bool) -> Option<bool> {
392    match tag {
393        Some(tag) => tag.codec_bool(),
394        None => Some(default),
395    }
396}
397
398fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
399    let mut key_hasher = ComponentHasher::new();
400    key_hasher.put_string(key);
401    let mut value_hasher = ComponentHasher::new();
402    value.hash_component(&mut value_hasher);
403    entries.push(HashEntry::new(key_hasher, value_hasher));
404}
405
406fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
407    sort_map_entries(entries);
408    hasher.start_map();
409    for entry in entries {
410        hasher.put_raw_bytes(&entry.key_bytes);
411        hasher.put_raw_bytes(&entry.value_bytes);
412    }
413    hasher.end_map();
414}
415
416#[cfg(test)]
417mod tests {
418    use std::io::Cursor;
419
420    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
421    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
422
423    use super::{MobEffectInstance, MobEffectInstanceDetails};
424    use crate::{REGISTRY, RegistryExt};
425    use crate::{init_vanilla_registry, vanilla_mob_effects};
426
427    /// The infinite-duration sentinel (`-1`) and a zero duration bypass the mapper entirely.
428    #[test]
429    fn map_duration_leaves_infinite_and_zero_sentinels_untouched() {
430        init_vanilla_registry();
431        let speed = REGISTRY
432            .mob_effects
433            .by_key(&steel_utils::Identifier::vanilla_static("speed"))
434            .expect("speed should be registered");
435        let lasting = |duration| MobEffectInstance::simple(speed, duration, 0);
436
437        assert_eq!(lasting(-1).map_duration(|duration| duration * 2), -1);
438        assert_eq!(lasting(0).map_duration(|duration| duration * 2), 0);
439        assert_eq!(lasting(10).map_duration(|duration| duration * 2), 20);
440
441        assert!(lasting(-1).is_infinite_duration());
442        assert!(!lasting(0).is_infinite_duration());
443    }
444
445    #[test]
446    fn with_scaled_duration_leaves_infinite_and_zero_durations_untouched() {
447        init_vanilla_registry();
448        let lasting = |duration| MobEffectInstance::simple(vanilla_mob_effects::LUCK, duration, 0);
449        let scaled = |duration, scale| lasting(duration).with_scaled_duration(scale).duration();
450
451        assert_eq!(scaled(-1, 0.5), -1);
452        assert_eq!(scaled(0, 0.5), 0);
453        // Even an extreme scale must not touch these sentinels.
454        assert_eq!(scaled(-1, 100.0), -1);
455        assert_eq!(scaled(0, 0.0), 0);
456    }
457
458    /// Vanilla computes `Math.max(Mth.floor(duration * scale), 1)`: a finite
459    /// duration is floor-scaled and never rounds below 1 tick, even when the
460    /// scale would floor it to 0.
461    #[test]
462    fn with_scaled_duration_floors_and_clamps_finite_durations() {
463        init_vanilla_registry();
464        let lasting = |duration| MobEffectInstance::simple(vanilla_mob_effects::LUCK, duration, 0);
465        let scaled = |duration, scale| lasting(duration).with_scaled_duration(scale).duration();
466
467        assert_eq!(scaled(100, 0.5), 50);
468        // floor(9 * 0.34) == floor(3.06) == 3, not a naive round to 3.
469        assert_eq!(scaled(9, 0.34), 3);
470        // A scale that would floor to 0 is clamped up to the 1-tick floor.
471        assert_eq!(scaled(1, 0.1), 1);
472        assert_eq!(scaled(100, 1.0), 100);
473    }
474
475    #[test]
476    fn effect_instances_round_trip_recursive_details_and_clamp_amplifier() {
477        init_vanilla_registry();
478        let speed = REGISTRY
479            .mob_effects
480            .by_key(&steel_utils::Identifier::vanilla_static("speed"))
481            .expect("speed should be registered");
482        let value = MobEffectInstance::new(
483            speed,
484            200,
485            300,
486            false,
487            true,
488            false,
489            Some(MobEffectInstanceDetails::new(
490                1, 400, true, false, false, None,
491            )),
492        );
493        assert_eq!(value.amplifier(), 255);
494        let nbt = value.clone().to_nbt_tag();
495        let mut bytes = Vec::new();
496        nbt.write(&mut bytes);
497        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice()))
498            .expect("effect NBT should parse");
499        assert_eq!(
500            MobEffectInstance::from_nbt_tag(borrowed.as_tag()),
501            Some(value.clone())
502        );
503
504        let mut network = Vec::new();
505        value.write(&mut network).expect("effect should encode");
506        assert_eq!(
507            MobEffectInstance::read(&mut Cursor::new(network.as_slice()))
508                .expect("effect should decode"),
509            value
510        );
511    }
512
513    #[test]
514    fn effect_instance_equality_ignores_hidden_effects_like_vanilla() {
515        init_vanilla_registry();
516        let speed = REGISTRY
517            .mob_effects
518            .by_key(&steel_utils::Identifier::vanilla_static("speed"))
519            .expect("speed should be registered");
520        let without_hidden = MobEffectInstance::simple(speed, 200, 1);
521        let with_hidden = MobEffectInstance::new(
522            speed,
523            200,
524            1,
525            false,
526            true,
527            true,
528            Some(MobEffectInstanceDetails::new(
529                2, 400, false, true, true, None,
530            )),
531        );
532
533        assert_eq!(with_hidden, without_hidden);
534    }
535}