Skip to main content

steel_protocol/packets/game/
c_sound.rs

1use glam::{DVec3, IVec3};
2use steel_macros::{ClientPacket, WriteTo};
3use steel_registry::packets::play::C_SOUND;
4use steel_registry::sound_event::{SoundEventHolder, SoundEventRef};
5use steel_utils::codec::VarInt;
6use steel_utils::serial::WriteTo as WriteToTrait;
7
8/// Sound source categories (matches vanilla `SoundSource` enum order).
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[repr(u8)]
11pub enum SoundSource {
12    Master = 0,
13    Music = 1,
14    Records = 2,
15    Weather = 3,
16    Blocks = 4,
17    Hostile = 5,
18    Neutral = 6,
19    Players = 7,
20    Ambient = 8,
21    Voice = 9,
22    Ui = 10,
23}
24
25impl SoundSource {
26    pub const VALUES: [SoundSource; 11] = [
27        SoundSource::Master,
28        SoundSource::Music,
29        SoundSource::Records,
30        SoundSource::Weather,
31        SoundSource::Blocks,
32        SoundSource::Hostile,
33        SoundSource::Neutral,
34        SoundSource::Players,
35        SoundSource::Ambient,
36        SoundSource::Voice,
37        SoundSource::Ui,
38    ];
39
40    /// Returns the vanilla command literal for this category.
41    #[must_use]
42    pub const fn name(self) -> &'static str {
43        match self {
44            Self::Master => "master",
45            Self::Music => "music",
46            Self::Records => "record",
47            Self::Weather => "weather",
48            Self::Blocks => "block",
49            Self::Hostile => "hostile",
50            Self::Neutral => "neutral",
51            Self::Players => "player",
52            Self::Ambient => "ambient",
53            Self::Voice => "voice",
54            Self::Ui => "ui",
55        }
56    }
57
58    /// Returns the `VarInt` value for the enum.
59    #[must_use]
60    pub const fn as_varint(self) -> i32 {
61        self as i32
62    }
63}
64
65impl WriteToTrait for SoundSource {
66    fn write(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
67        VarInt(*self as i32).write(writer)
68    }
69}
70
71/// Sent to play a sound effect at a specific position.
72///
73/// The position is encoded at 8x precision (divide by 8 to get actual block coordinates).
74/// This allows sub-block positioning for more accurate sound placement.
75#[derive(WriteTo, ClientPacket, Clone, Debug)]
76#[packet_id(Play = C_SOUND)]
77pub struct CSound {
78    /// The holder-encoded sound event.
79    pub sound: SoundEventHolder,
80    /// The sound source category.
81    pub source: SoundSource,
82    /// X position multiplied by 8 (fixed-point).
83    pub pos: IVec3,
84    /// Volume (1.0 = normal).
85    pub volume: f32,
86    /// Pitch (1.0 = normal).
87    pub pitch: f32,
88    /// Random seed for sound variations.
89    pub seed: i64,
90}
91
92impl CSound {
93    /// Creates a new sound packet.
94    ///
95    /// # Arguments
96    /// * `sound` - Sound event to play
97    /// * `source` - Sound source category
98    /// * `x`, `y`, `z` - Position in block coordinates (will be scaled by 8)
99    /// * `volume` - Volume multiplier (1.0 = normal)
100    /// * `pitch` - Pitch multiplier (1.0 = normal)
101    /// * `seed` - Random seed for sound variations
102    #[must_use]
103    pub fn new(
104        sound: SoundEventRef,
105        source: SoundSource,
106        pos: DVec3,
107        volume: f32,
108        pitch: f32,
109        seed: i64,
110    ) -> Self {
111        Self::new_holder(
112            SoundEventHolder::registry(sound),
113            source,
114            pos,
115            volume,
116            pitch,
117            seed,
118        )
119    }
120
121    /// Creates a sound packet from a registered or direct sound holder.
122    #[must_use]
123    pub fn new_holder(
124        sound: SoundEventHolder,
125        source: SoundSource,
126        pos: DVec3,
127        volume: f32,
128        pitch: f32,
129        seed: i64,
130    ) -> Self {
131        Self {
132            sound,
133            source,
134            pos: IVec3::new(
135                (pos.x * 8.0) as i32,
136                (pos.y * 8.0) as i32,
137                (pos.z * 8.0) as i32,
138            ),
139            volume,
140            pitch,
141            seed,
142        }
143    }
144
145    /// Creates a block sound packet at the center of a block position.
146    ///
147    /// # Arguments
148    /// * `sound` - Sound event to play
149    /// * `pos` - Block position (will be centered at +0.5)
150    /// * `volume` - Volume multiplier
151    /// * `pitch` - Pitch multiplier
152    /// * `seed` - Random seed
153    #[must_use]
154    pub fn block_sound(
155        sound: SoundEventRef,
156        pos: steel_utils::BlockPos,
157        volume: f32,
158        pitch: f32,
159        seed: i64,
160    ) -> Self {
161        Self::new(
162            sound,
163            SoundSource::Blocks,
164            pos.0.as_dvec3().map(|v| v + 0.5),
165            volume,
166            pitch,
167            seed,
168        )
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use steel_registry::init_vanilla_registry;
175    use steel_registry::{RegistryEntry, sound_events};
176    use steel_utils::BlockPos;
177
178    use super::CSound;
179
180    #[test]
181    fn registered_sound_packet_uses_holder_id() {
182        init_vanilla_registry();
183
184        let packet = CSound::block_sound(
185            &sound_events::BLOCK_WOODEN_BUTTON_CLICK_ON,
186            BlockPos::ZERO,
187            1.0,
188            1.0,
189            0,
190        );
191
192        let expected_holder_id = sound_events::BLOCK_WOODEN_BUTTON_CLICK_ON.id() as i32 + 1;
193        assert_eq!(
194            sound_events::BLOCK_WOODEN_BUTTON_CLICK_ON.packet_holder_id(),
195            expected_holder_id
196        );
197        assert!(matches!(
198            packet.sound,
199            steel_registry::sound_event::SoundEventHolder::Registry(sound)
200                if sound.packet_holder_id() == expected_holder_id
201        ));
202    }
203}