use crate::data_component_impl::basic::SoundEvent; use crate::data_component_impl::{ DataComponentImpl, IDSet, IdOr, default_impl, get_f32_hash, get_i32_hash, get_idor, get_idor_hash, get_idset_hash, get_str_hash, put_idor, }; use crate::effect::StatusEffect; use crate::sound::Sound; use crc_fast::CrcAlgorithm::Crc32Iscsi; use crc_fast::Digest; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::str::FromStr; #[derive(Clone, Debug, PartialEq)] pub struct FoodImpl { pub nutrition: i32, pub saturation: f32, pub can_always_eat: bool, } impl DataComponentImpl for FoodImpl { default_impl!(Food); } impl Hash for FoodImpl { fn hash(&self, state: &mut H) { self.nutrition.hash(state); unsafe { (*(&raw const self.saturation).cast::()).hash(state) }; self.can_always_eat.hash(state); } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct StatusEffectInstance { pub effect_id: Cow<'static, str>, pub amplifier: i32, pub duration: i32, pub ambient: bool, pub show_particles: bool, pub show_icon: bool, } impl StatusEffectInstance { pub fn read_data(nbt: &NbtTag) -> Option { let compound = nbt.extract_compound()?; let effect_id = Cow::Owned(compound.get_string("id")?.to_string()); let amplifier = compound.get_int("amplifier")?; let duration = compound.get_int("duration")?; let ambient = compound.get_bool("ambient")?; let show_particles = compound.get_bool("show_particles")?; let show_icon = compound.get_bool("show_icon")?; Some(Self { effect_id, amplifier, duration, ambient, show_particles, show_icon, }) } pub fn as_nbt(&self) -> NbtTag { let mut compound = NbtCompound::new(); compound.put_string("id", self.effect_id.to_string()); compound.put_int("amplifier", self.amplifier); compound.put_int("duration", self.duration); compound.put_bool("ambient", self.ambient); compound.put_bool("show_particles", self.show_particles); compound.put_bool("show_icon", self.show_icon); NbtTag::Compound(compound) } pub fn get_hash(&self) -> i32 { let mut digest = Digest::new(Crc32Iscsi); digest.update(&get_str_hash(self.effect_id.as_ref()).to_le_bytes()); digest.update(&get_i32_hash(self.amplifier).to_le_bytes()); digest.update(&get_i32_hash(self.duration).to_le_bytes()); digest.update(&[self.ambient as u8]); digest.update(&[self.show_particles as u8]); digest.update(&[self.show_icon as u8]); digest.finalize() as i32 } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ConsumeAnimation { None, Eat, Drink, Block, Bow, Spear, Crossbow, Spyglass, Horn, Brush, } impl ConsumeAnimation { #[must_use] pub const fn to_str(&self) -> &'static str { match self { ConsumeAnimation::None => "none", ConsumeAnimation::Eat => "eat", ConsumeAnimation::Drink => "drink", ConsumeAnimation::Block => "block", ConsumeAnimation::Bow => "bow", ConsumeAnimation::Spear => "spear", ConsumeAnimation::Crossbow => "crossbow", ConsumeAnimation::Spyglass => "spyglass", ConsumeAnimation::Horn => "horn", ConsumeAnimation::Brush => "brush", } } } impl TryFrom for ConsumeAnimation { type Error = (); fn try_from(value: i32) -> Result { match value { 0 => Ok(Self::None), 1 => Ok(Self::Eat), 2 => Ok(Self::Drink), 3 => Ok(Self::Block), 4 => Ok(Self::Bow), 5 => Ok(Self::Spear), 6 => Ok(Self::Crossbow), 7 => Ok(Self::Spyglass), 8 => Ok(Self::Horn), 9 => Ok(Self::Brush), _ => Err(()), } } } impl FromStr for ConsumeAnimation { type Err = (); fn from_str(s: &str) -> Result { match s { "none" => Ok(Self::None), "eat" => Ok(Self::Eat), "drink" => Ok(Self::Drink), "block" => Ok(Self::Block), "bow" => Ok(Self::Bow), "spear" => Ok(Self::Spear), "crossbow" => Ok(Self::Crossbow), "spyglass" => Ok(Self::Spyglass), "horn" => Ok(Self::Horn), "brush" => Ok(Self::Brush), _ => Err(()), } } } #[derive(Clone, Debug, PartialEq)] pub enum ConsumeEffect { ApplyEffects((Cow<'static, [StatusEffectInstance]>, f32)), RemoveEffects(IDSet), ClearAllEffects, TeleportRandomly(f32), PlaySound(IdOr), } impl Hash for ConsumeEffect { fn hash(&self, state: &mut H) { self.to_str().hash(state); match self { ConsumeEffect::ApplyEffects(tuple) => { tuple.0.hash(state); unsafe { (*(&raw const tuple.1).cast::()).hash(state) }; } ConsumeEffect::RemoveEffects(status_effect_instances) => { status_effect_instances.hash(state) } ConsumeEffect::ClearAllEffects => (), ConsumeEffect::TeleportRandomly(dst) => unsafe { (*(&raw const dst).cast::()).hash(state) }, ConsumeEffect::PlaySound(id_or) => id_or.hash(state), } } } impl ConsumeEffect { pub fn to_str(&self) -> &str { match self { ConsumeEffect::ApplyEffects(_) => "apply_effects", ConsumeEffect::RemoveEffects(_) => "remove_effects", ConsumeEffect::ClearAllEffects => "clear_all_effects", ConsumeEffect::TeleportRandomly(_) => "teleport_randomly", ConsumeEffect::PlaySound(_) => "play_sound", } } pub fn registry_id(&self) -> u8 { match self { ConsumeEffect::ApplyEffects(_) => 0, ConsumeEffect::RemoveEffects(_) => 1, ConsumeEffect::ClearAllEffects => 2, ConsumeEffect::TeleportRandomly(_) => 3, ConsumeEffect::PlaySound(_) => 4, } } pub fn read_data(nbt: &NbtTag) -> Option { let compound = nbt.extract_compound()?; let r#type = compound.get_string("type")?; match r#type { "remove_effects" => { let idset = IDSet::read(compound.get("effects")?)?; Some(Self::RemoveEffects(idset)) } "clear_all_effects" => Some(Self::ClearAllEffects), "teleport_randomly" => { let dst = compound.get_float("diameter")?; Some(Self::TeleportRandomly(dst)) } "play_sound" => { let sound = get_idor(compound, "sound", Sound::EntityGenericEat); Some(Self::PlaySound(sound)) } "apply_effects" => { let probability = compound.get_float("probability")?; let effects_vec: Vec = compound .get_list("effects")? .iter() .filter_map(StatusEffectInstance::read_data) .collect(); let effects: Cow<'static, [StatusEffectInstance]> = Cow::Owned(effects_vec); Some(Self::ApplyEffects((effects, probability))) } _ => None, } } pub fn as_nbt(&self) -> NbtTag { let mut compound = NbtCompound::new(); compound.put_string("type", self.to_str().to_string()); match self { ConsumeEffect::ApplyEffects(data) => { let nbt_arr = data.0.iter().map(|x| x.as_nbt()).collect(); compound.put_list("effects", nbt_arr); compound.put_float("probability", data.1); } ConsumeEffect::RemoveEffects(idset) => idset.write(&mut compound, "effects"), ConsumeEffect::ClearAllEffects => (), ConsumeEffect::TeleportRandomly(dst) => compound.put_float("diameter", *dst), ConsumeEffect::PlaySound(id_or) => { put_idor(&mut compound, "sound", id_or); } } NbtTag::Compound(compound) } pub fn get_hash(&self) -> i32 { let mut digest = Digest::new(Crc32Iscsi); match self { ConsumeEffect::ApplyEffects((effects, probability)) => { digest.update(&[1u8]); for effect in effects.iter() { digest.update(&effect.get_hash().to_le_bytes()); } digest.update(&[13u8]); digest.update(&get_f32_hash(*probability).to_le_bytes()); } ConsumeEffect::RemoveEffects(idset) => { digest.update(&[2u8]); digest.update(&get_idset_hash(idset).to_le_bytes()); } ConsumeEffect::ClearAllEffects => { digest.update(&[3u8]); } ConsumeEffect::TeleportRandomly(dst) => { digest.update(&[4u8]); digest.update(&get_f32_hash(*dst).to_le_bytes()); } ConsumeEffect::PlaySound(id_or) => { digest.update(&[5u8]); digest.update(&get_idor_hash(id_or).to_le_bytes()); } } digest.finalize() as i32 } } #[derive(Clone, Debug, PartialEq)] pub struct ConsumableImpl { pub consume_seconds: f32, pub animation: ConsumeAnimation, pub sound_event: IdOr, pub consume_particles: bool, pub effects: Cow<'static, [ConsumeEffect]>, } impl ConsumableImpl { pub const fn new( consume_seconds: f32, animation: ConsumeAnimation, sound_event: IdOr, consume_particles: bool, effects: Cow<'static, [ConsumeEffect]>, ) -> Self { Self { consume_seconds, animation, sound_event, consume_particles, effects, } } #[must_use] pub fn consume_ticks(&self) -> i32 { (self.consume_seconds * 20.0) as i32 } pub fn read_data(data: &NbtTag) -> Option { let compound = data.extract_compound()?; let consume_seconds = compound.get_float("consume_seconds")?; let animation = compound .get_string("animation")? .parse::() .ok()?; let sound_event = get_idor(compound, "sound", Sound::EntityGenericEat); let consume_particles = compound.get_bool("has_consume_particles").unwrap_or(false); let opt_list = compound.get_list("on_consume_effects"); let effects: Cow<'static, [ConsumeEffect]> = if let Some(effect_list) = opt_list { effect_list .iter() .filter_map(ConsumeEffect::read_data) .collect() } else { Cow::Borrowed(&[]) }; Some(Self { consume_seconds, animation, sound_event, consume_particles, effects, }) } } impl DataComponentImpl for ConsumableImpl { fn write_data(&self) -> NbtTag { let mut compound = NbtCompound::new(); compound.put_float("consume_seconds", self.consume_seconds); compound.put_string("animation", self.animation.to_str().to_string()); put_idor(&mut compound, "sound", &self.sound_event); compound.put_bool("has_consume_particles", self.consume_particles); let nbt_vec = self.effects.iter().map(|x| x.as_nbt()).collect(); compound.put_list("on_consume_effects", nbt_vec); NbtTag::Compound(compound) } fn get_hash(&self) -> i32 { let mut digest = Digest::new(Crc32Iscsi); digest.update(&[2u8]); digest.update(&get_f32_hash(self.consume_seconds).to_le_bytes()); digest.update(&get_i32_hash(self.animation as i32).to_le_bytes()); digest.update(&get_idor_hash(&self.sound_event).to_be_bytes()); digest.update(&[self.consume_particles as u8]); for effect in self.effects.iter() { digest.update(&effect.get_hash().to_le_bytes()); } digest.update(&[3u8]); digest.finalize() as i32 } default_impl!(Consumable); } impl Hash for ConsumableImpl { fn hash(&self, state: &mut H) { unsafe { (*(&raw const self.consume_seconds).cast::()).hash(state) }; } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct UseEffectsImpl; impl DataComponentImpl for UseEffectsImpl { default_impl!(UseEffects); } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct UseRemainderImpl; impl DataComponentImpl for UseRemainderImpl { default_impl!(UseRemainder); } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UseCooldownImpl { pub seconds: f32, pub cooldown_group: Option, } impl UseCooldownImpl { pub fn new(seconds: f32, cooldown_group: Option) -> Self { Self { seconds, cooldown_group, } } pub fn read_data(data: &NbtTag) -> Option { let compound = data.extract_compound()?; let seconds = compound.get_float("seconds")?; let cooldown_group = compound.get_string("cooldown_group").map(|s| s.to_string()); Some(Self { seconds, cooldown_group, }) } } impl DataComponentImpl for UseCooldownImpl { fn write_data(&self) -> NbtTag { let mut compound = NbtCompound::new(); compound.put_float("seconds", self.seconds); if let Some(group) = &self.cooldown_group { compound.put_string("cooldown_group", group.clone()); } NbtTag::Compound(compound) } fn get_hash(&self) -> i32 { let mut digest = Digest::new(Crc32Iscsi); digest.update(&get_f32_hash(self.seconds).to_le_bytes()); if let Some(group) = &self.cooldown_group { digest.update(&get_str_hash(group).to_le_bytes()); } digest.finalize() as i32 } default_impl!(UseCooldown); } impl Eq for UseCooldownImpl {} impl Hash for UseCooldownImpl { fn hash(&self, state: &mut H) { self.seconds.to_bits().hash(state); self.cooldown_group.hash(state); } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct PotionContentsImpl { pub potion_id: Option, pub custom_color: Option, pub custom_effects: Vec, pub custom_name: Option, } impl PotionContentsImpl { pub fn read_data(tag: &NbtTag) -> Option { let compound = tag.extract_compound()?; let potion_id = if let Some(id) = compound.get_int("potion") { Some(id) } else if let Some(name) = compound.get_string("potion") { let name = name.strip_prefix("minecraft:").unwrap_or(name); crate::potion::Potion::from_name(name).map(|p| p.id as i32) } else { None }; let custom_color = compound.get_int("custom_color"); let custom_name = compound.get_string("custom_name").map(|s| s.to_string()); let custom_effects = compound .get_list("custom_effects") .map(|list| { list.iter() .filter_map(|item| { let effect_tag = item.extract_compound()?; let id: Cow<'static, str> = Cow::Owned(effect_tag.get_string("id")?.to_string()); let amplifier = effect_tag .get_int("amplifier") .or_else(|| effect_tag.get_byte("amplifier").map(i32::from)) .unwrap_or(0); let duration = effect_tag .get_int("duration") .or_else(|| effect_tag.get_byte("duration").map(i32::from)) .unwrap_or(0); let ambient = effect_tag.get_bool("ambient").unwrap_or(false); let show_particles = effect_tag.get_bool("show_particles").unwrap_or(true); let show_icon = effect_tag.get_bool("show_icon").unwrap_or(true); Some(StatusEffectInstance { effect_id: id, amplifier, duration, ambient, show_particles, show_icon, }) }) .collect::>() }) .unwrap_or_default(); Some(Self { potion_id, custom_color, custom_effects, custom_name, }) } } impl DataComponentImpl for PotionContentsImpl { fn write_data(&self) -> NbtTag { let mut compound = NbtCompound::new(); if let Some(potion_id) = self.potion_id { compound.put_int("potion", potion_id); } if let Some(color) = self.custom_color { compound.put_int("custom_color", color); } if !self.custom_effects.is_empty() { let mut effects_list = Vec::new(); for effect in &self.custom_effects { let mut effect_compound = NbtCompound::new(); effect_compound.put_string("id", effect.effect_id.to_string()); effect_compound.put_int("amplifier", effect.amplifier); effect_compound.put_int("duration", effect.duration); effect_compound.put_byte("ambient", effect.ambient as i8); effect_compound.put_byte("show_particles", effect.show_particles as i8); effect_compound.put_byte("show_icon", effect.show_icon as i8); effects_list.push(NbtTag::Compound(effect_compound)); } compound.put("custom_effects", NbtTag::List(effects_list)); } if let Some(name) = &self.custom_name { compound.put_string("custom_name", name.clone()); } NbtTag::Compound(compound) } fn get_hash(&self) -> i32 { let mut digest = Digest::new(Crc32Iscsi); if let Some(id) = self.potion_id { digest.update(&[1u8]); digest.update(&get_i32_hash(id).to_le_bytes()); } if let Some(color) = self.custom_color { digest.update(&[2u8]); digest.update(&get_i32_hash(color).to_le_bytes()); } if let Some(name) = &self.custom_name { digest.update(&[3u8]); digest.update(&get_str_hash(name).to_le_bytes()); } if !self.custom_effects.is_empty() { digest.update(&[4u8]); for effect in &self.custom_effects { digest.update(&effect.get_hash().to_le_bytes()); } } digest.finalize() as i32 } default_impl!(PotionContents); } #[derive(Clone, Debug, PartialEq)] pub struct PotionDurationScaleImpl { pub scale: f32, } impl PotionDurationScaleImpl { pub fn read_data(data: &NbtTag) -> Option { data.extract_float().map(|scale| Self { scale }) } } impl DataComponentImpl for PotionDurationScaleImpl { fn write_data(&self) -> NbtTag { NbtTag::Float(self.scale) } fn get_hash(&self) -> i32 { get_f32_hash(self.scale) as i32 } default_impl!(PotionDurationScale); } impl Hash for PotionDurationScaleImpl { fn hash(&self, state: &mut H) { self.scale.to_bits().hash(state); } } #[cfg(test)] mod tests { use super::{DataComponentImpl, PotionDurationScaleImpl}; use crate::item::Item; #[test] fn potion_duration_scale_round_trips_as_a_float() { let scale = PotionDurationScaleImpl { scale: 0.125 }; let encoded = scale.write_data(); let decoded = PotionDurationScaleImpl::read_data(&encoded).expect("scale should decode"); assert_eq!(decoded, scale); } #[test] fn generated_arrow_duration_scale_is_data_driven() { let scale = Item::TIPPED_ARROW .components .iter() .find_map(|(id, component)| { (*id == crate::data_component::DataComponent::PotionDurationScale) .then(|| component.as_any().downcast_ref::()) .flatten() }) .expect("tipped arrows should have a duration scale"); assert_eq!(scale.scale, 0.125); } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct SuspiciousStewEffectsImpl; impl DataComponentImpl for SuspiciousStewEffectsImpl { default_impl!(SuspiciousStewEffects); }