1use 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
19pub const INFINITE_EFFECT_DURATION: i32 = -1;
21
22#[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#[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 #[must_use]
89 pub const fn is_infinite_duration(&self) -> bool {
90 self.duration == INFINITE_EFFECT_DURATION
91 }
92
93 #[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 #[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 #[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 assert_eq!(scaled(-1, 100.0), -1);
455 assert_eq!(scaled(0, 0.0), 0);
456 }
457
458 #[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 assert_eq!(scaled(9, 0.34), 3);
470 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}