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
19#[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#[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}