187 lines
5.6 KiB
Rust
187 lines
5.6 KiB
Rust
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<SoundEvent>,
|
|
/// 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<SoundEvent>,
|
|
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::<crate::SoundEvent>::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<u8>) -> 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));
|
|
}
|
|
}
|