use std::io::Write; use pumpkin_data::{ packet::clientbound::play::SOUND_ENTITY, sound::SoundCategory, sound_id_remap::remap_sound_id_for_version, }; use pumpkin_macros::java_packet; use pumpkin_util::version::JavaMinecraftVersion; use crate::{ClientPacket, IdOr, SoundEvent, VarInt, WritingError, ser::NetworkWriteExt}; /// Plays a sound effect that originates from a specific entity. /// /// Unlike global sounds, this sound will follow the entity as it moves /// through the world. The client handles the panning and attenuation /// (volume drop-off) based on the distance between the player and the entity. #[java_packet(SOUND_ENTITY)] pub struct CEntitySoundEffect { /// The sound to play. Can be a hardcoded ID or a custom `SoundEvent` /// (Resource Location). pub sound_event: IdOr, /// The category of the sound (e.g., Master, Music, Weather, Players). /// Used by the client to apply volume sliders from settings. pub sound_category: VarInt, /// The Entity ID that the sound is "attached" to. pub entity_id: VarInt, /// The loudness of the sound (usually 1.0). pub volume: f32, /// The playback speed/pitch (0.5 to 2.0). pub pitch: f32, /// A random seed used for sound variations (like different pitch shifts /// for the same sound). pub seed: i64, } impl CEntitySoundEffect { #[must_use] pub const fn new( sound_event: IdOr, sound_category: SoundCategory, entity_id: VarInt, volume: f32, pitch: f32, seed: i64, ) -> Self { Self { sound_event, sound_category: VarInt(sound_category as i32), entity_id, volume, pitch, seed, } } } impl ClientPacket for CEntitySoundEffect { fn write_packet_data( &self, mut write: impl Write, version: &JavaMinecraftVersion, ) -> Result<(), WritingError> { if *version >= JavaMinecraftVersion::V_1_19_3 { let sound_event = match &self.sound_event { IdOr::Id(id) => IdOr::Id(remap_sound_id_for_version(*id, *version)), IdOr::Value(value) => IdOr::Value(value.clone()), }; crate::IdOr::::write(&sound_event, &mut write, |w, e| { w.write_string(&e.sound_name)?; w.write_option(&e.range, |w2, r| w2.write_f32(*r)) })?; } else { let sound_id = match &self.sound_event { IdOr::Id(id) => remap_sound_id_for_version(*id, *version), IdOr::Value(_) => 0, }; write.write_var_int(&VarInt(i32::from(sound_id)))?; } write.write_var_int(&self.sound_category)?; write.write_var_int(&self.entity_id)?; write.write_f32(self.volume)?; if *version >= JavaMinecraftVersion::V_1_10 { write.write_f32(self.pitch)?; } else { let pitch_byte = (self.pitch * 63.0).round().clamp(0.0, 255.0) as u8; write.write_u8(pitch_byte)?; } if *version >= JavaMinecraftVersion::V_1_19 { write.write_i64(self.seed)?; } Ok(()) } } #[cfg(test)] mod tests { use std::io::Cursor; use pumpkin_data::sound::SoundCategory; use pumpkin_data::sound_id_remap::remap_sound_id_for_version; use pumpkin_util::version::JavaMinecraftVersion; use crate::{ClientPacket, IdOr, SoundEvent, VarInt}; use super::CEntitySoundEffect; fn first_remapped_sound_id(version: JavaMinecraftVersion) -> u16 { (0..=u16::MAX) .find(|id| remap_sound_id_for_version(*id, version) != *id) .expect("sound remap table should contain at least one changed id") } fn first_var_int(bytes: Vec) -> VarInt { VarInt::decode(&mut Cursor::new(bytes)).unwrap() } #[test] fn numeric_sound_id_remaps_for_1_21_11() { let sound_id = first_remapped_sound_id(JavaMinecraftVersion::V_1_21_11); let packet = CEntitySoundEffect::new( IdOr::Id(sound_id), SoundCategory::Players, VarInt(123), 1.0, 1.0, 42, ); let mut bytes = Vec::new(); packet .write_packet_data(&mut bytes, &JavaMinecraftVersion::V_1_21_11) .unwrap(); assert_eq!( first_var_int(bytes), VarInt::from(remap_sound_id_for_version(sound_id, JavaMinecraftVersion::V_1_21_11) + 1) ); } #[test] fn numeric_sound_id_stays_latest_for_26_2() { let sound_id = first_remapped_sound_id(JavaMinecraftVersion::V_1_21_11); let packet = CEntitySoundEffect::new( IdOr::Id(sound_id), SoundCategory::Players, VarInt(123), 1.0, 1.0, 42, ); let mut bytes = Vec::new(); packet .write_packet_data(&mut bytes, &JavaMinecraftVersion::V_26_2) .unwrap(); assert_eq!(first_var_int(bytes), VarInt::from(sound_id + 1)); } #[test] fn direct_sound_event_keeps_direct_holder_encoding() { let packet = CEntitySoundEffect::new( IdOr::Value(SoundEvent { sound_name: "minecraft:test.sound".to_string(), range: None, }), SoundCategory::Players, VarInt(123), 1.0, 1.0, 42, ); let mut bytes = Vec::new(); packet .write_packet_data(&mut bytes, &JavaMinecraftVersion::V_1_21_11) .unwrap(); assert_eq!(first_var_int(bytes), VarInt::from(0)); } }