Skip to main content

steel_registry/
sound_event.rs

1use rustc_hash::FxHashMap;
2use simdnbt::owned::{NbtCompound, NbtTag};
3use simdnbt::{FromNbtTag, ToNbtTag};
4use std::io::{Cursor, Error, Result, Write};
5use std::str::FromStr;
6use steel_utils::codec::VarInt;
7use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
8use steel_utils::serial::{ReadFrom, WriteTo};
9use steel_utils::{DowncastType, DowncastTypeKey, Identifier};
10
11use crate::{REGISTRY, RegistryEntry, RegistryExt};
12
13/// Built-in sound event registry entry used by sound packets and data-driven audio refs.
14#[derive(Debug)]
15pub struct SoundEvent {
16    pub key: Identifier,
17    pub sound_id: Identifier,
18    pub fixed_range: Option<f32>,
19}
20
21impl SoundEvent {
22    #[must_use]
23    pub fn range(&self, volume: f32) -> f32 {
24        self.fixed_range
25            .unwrap_or(if volume > 1.0 { 16.0 * volume } else { 16.0 })
26    }
27
28    /// Returns the `VarInt` payload used by vanilla holder-based sound packets.
29    #[must_use]
30    pub fn packet_holder_id(&self) -> i32 {
31        let id = crate::RegistryEntry::id(self);
32        assert!(
33            id < i32::MAX as usize,
34            "sound event registry id exceeds protocol VarInt range"
35        );
36        id as i32 + 1
37    }
38}
39
40pub type SoundEventRef = &'static SoundEvent;
41
42#[derive(Debug, Clone)]
43pub enum SoundEventHolder {
44    Registry(SoundEventRef),
45    Direct {
46        sound_id: Identifier,
47        fixed_range: Option<f32>,
48    },
49}
50
51impl PartialEq for SoundEventHolder {
52    fn eq(&self, other: &Self) -> bool {
53        match (self, other) {
54            (Self::Registry(left), Self::Registry(right)) => left == right,
55            (
56                Self::Direct {
57                    sound_id: left_id,
58                    fixed_range: left_range,
59                },
60                Self::Direct {
61                    sound_id: right_id,
62                    fixed_range: right_range,
63                },
64            ) => left_id == right_id && optional_float_equals(*left_range, *right_range),
65            (Self::Registry(_), Self::Direct { .. }) | (Self::Direct { .. }, Self::Registry(_)) => {
66                false
67            }
68        }
69    }
70}
71
72const fn optional_float_equals(left: Option<f32>, right: Option<f32>) -> bool {
73    match (left, right) {
74        (Some(left), Some(right)) => {
75            (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
76        }
77        (None, None) => true,
78        (Some(_), None) | (None, Some(_)) => false,
79    }
80}
81
82// SAFETY: This Steel-owned key uniquely identifies `SoundEventHolder` within
83// the linked process.
84unsafe impl DowncastType for SoundEventHolder {
85    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:registry/sound_event_holder");
86}
87
88impl SoundEventHolder {
89    #[must_use]
90    pub const fn registry(sound: SoundEventRef) -> Self {
91        Self::Registry(sound)
92    }
93
94    /// Returns the vanilla playback range for this sound holder.
95    #[must_use]
96    pub fn range(&self, volume: f32) -> f32 {
97        match self {
98            Self::Registry(sound) => sound.range(volume),
99            Self::Direct { fixed_range, .. } => {
100                fixed_range.unwrap_or(if volume > 1.0 { 16.0 * volume } else { 16.0 })
101            }
102        }
103    }
104
105    #[must_use]
106    pub const fn registry_ref(&self) -> Option<SoundEventRef> {
107        match self {
108            Self::Registry(sound) => Some(*sound),
109            Self::Direct { .. } => None,
110        }
111    }
112
113    pub(crate) fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
114        if let Some(value) = tag.string() {
115            let id = Identifier::from_str(&value.to_string()).ok()?;
116            return REGISTRY.sound_events.by_key(&id).map(Self::Registry);
117        }
118
119        let compound = tag.compound()?;
120        let sound_id =
121            Identifier::from_str(&compound.get("sound_id")?.string()?.to_string()).ok()?;
122        let fixed_range = compound
123            .get("range")
124            .and_then(steel_utils::nbt::NbtNumeric::codec_f32);
125        Some(Self::Direct {
126            sound_id,
127            fixed_range,
128        })
129    }
130}
131
132impl WriteTo for SoundEventHolder {
133    fn write(&self, writer: &mut impl Write) -> Result<()> {
134        match self {
135            Self::Registry(sound) => {
136                let id = sound
137                    .try_id()
138                    .ok_or_else(|| Error::other(format!("Unknown sound event: {}", sound.key)))?;
139                let id = i32::try_from(id).map_err(|_| {
140                    Error::other(format!("Sound event id out of protocol range: {id}"))
141                })?;
142                VarInt(id + 1).write(writer)
143            }
144            Self::Direct {
145                sound_id,
146                fixed_range,
147            } => {
148                VarInt(0).write(writer)?;
149                sound_id.write(writer)?;
150                fixed_range.write(writer)
151            }
152        }
153    }
154}
155
156impl ReadFrom for SoundEventHolder {
157    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
158        let holder_id = VarInt::read(data)?.0;
159        if holder_id == 0 {
160            return Ok(Self::Direct {
161                sound_id: Identifier::read(data)?,
162                fixed_range: Option::<f32>::read(data)?,
163            });
164        }
165        if holder_id < 0 {
166            return Err(Error::other(format!(
167                "Negative sound event holder id: {holder_id}"
168            )));
169        }
170
171        REGISTRY
172            .sound_events
173            .by_id((holder_id - 1) as usize)
174            .map(Self::Registry)
175            .ok_or_else(|| Error::other(format!("Unknown sound event holder id: {holder_id}")))
176    }
177}
178
179impl ToNbtTag for SoundEventHolder {
180    fn to_nbt_tag(self) -> NbtTag {
181        match self {
182            Self::Registry(sound) => sound.key.to_string().to_nbt_tag(),
183            Self::Direct {
184                sound_id,
185                fixed_range,
186            } => {
187                let mut compound = NbtCompound::new();
188                compound.insert("sound_id", sound_id.to_string());
189                if let Some(range) = fixed_range {
190                    compound.insert("range", range);
191                }
192                NbtTag::Compound(compound)
193            }
194        }
195    }
196}
197
198impl FromNbtTag for SoundEventHolder {
199    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
200        Self::from_owned_nbt(&tag.to_owned())
201    }
202}
203
204impl HashComponent for SoundEventHolder {
205    fn hash_component(&self, hasher: &mut ComponentHasher) {
206        match self {
207            Self::Registry(sound) => hasher.put_string(&sound.key.to_string()),
208            Self::Direct {
209                sound_id,
210                fixed_range,
211            } => {
212                let mut entries = Vec::new();
213                push_hash_entry(&mut entries, "sound_id", &sound_id.to_string());
214                if let Some(range) = fixed_range {
215                    push_hash_entry(&mut entries, "range", range);
216                }
217                sort_map_entries(&mut entries);
218                hasher.start_map();
219                for entry in &entries {
220                    hasher.put_raw_bytes(&entry.key_bytes);
221                    hasher.put_raw_bytes(&entry.value_bytes);
222                }
223                hasher.end_map();
224            }
225        }
226    }
227}
228
229fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
230    let mut key_hasher = ComponentHasher::new();
231    key_hasher.put_string(key);
232    let mut value_hasher = ComponentHasher::new();
233    value.hash_component(&mut value_hasher);
234    entries.push(HashEntry::new(key_hasher, value_hasher));
235}
236
237pub struct SoundEventRegistry {
238    sound_events_by_id: Vec<SoundEventRef>,
239    sound_events_by_key: FxHashMap<Identifier, usize>,
240    allows_registering: bool,
241}
242
243impl SoundEventRegistry {
244    #[must_use]
245    pub fn new() -> Self {
246        Self {
247            sound_events_by_id: Vec::new(),
248            sound_events_by_key: FxHashMap::default(),
249            allows_registering: true,
250        }
251    }
252}
253
254crate::impl_standard_methods!(
255    SoundEventRegistry,
256    SoundEventRef,
257    sound_events_by_id,
258    sound_events_by_key,
259    allows_registering
260);
261
262crate::impl_registry!(
263    SoundEventRegistry,
264    SoundEvent,
265    sound_events_by_id,
266    sound_events_by_key,
267    sound_events
268);
269
270#[cfg(test)]
271mod tests {
272    use std::io::Cursor;
273
274    use simdnbt::FromNbtTag;
275    use simdnbt::borrow::{NbtTag as BorrowedNbtTag, read_tag};
276    use simdnbt::owned::{NbtCompound, NbtTag};
277    use steel_utils::Identifier;
278
279    use super::SoundEventHolder;
280
281    fn with_borrowed_tag<R>(tag: NbtTag, visitor: impl FnOnce(BorrowedNbtTag<'_, '_>) -> R) -> R {
282        let mut bytes = Vec::new();
283        tag.write(&mut bytes);
284        let borrowed =
285            read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
286        visitor(borrowed.as_tag())
287    }
288
289    #[test]
290    fn direct_sound_range_uses_lenient_numeric_codec() {
291        let mut compound = NbtCompound::new();
292        compound.insert("sound_id", "minecraft:test");
293        compound.insert("range", 5.5_f64);
294        let sound = with_borrowed_tag(NbtTag::Compound(compound), SoundEventHolder::from_nbt_tag)
295            .expect("direct sound should parse");
296        assert!(matches!(
297            sound,
298            SoundEventHolder::Direct {
299                fixed_range: Some(5.5),
300                ..
301            }
302        ));
303
304        let mut malformed = NbtCompound::new();
305        malformed.insert("sound_id", "minecraft:test");
306        malformed.insert("range", "far");
307        let sound = with_borrowed_tag(NbtTag::Compound(malformed), SoundEventHolder::from_nbt_tag)
308            .expect("lenient optional range should be ignored");
309        assert!(matches!(
310            sound,
311            SoundEventHolder::Direct {
312                fixed_range: None,
313                ..
314            }
315        ));
316    }
317
318    #[test]
319    fn direct_sound_equality_matches_java_float_rules() {
320        let direct = |range| SoundEventHolder::Direct {
321            sound_id: Identifier::vanilla_static("test"),
322            fixed_range: Some(range),
323        };
324
325        assert_eq!(direct(f32::NAN), direct(f32::NAN));
326        assert_ne!(direct(0.0), direct(-0.0));
327    }
328
329    #[test]
330    fn sound_holder_range_matches_variable_and_fixed_ranges() {
331        let variable = SoundEventHolder::Direct {
332            sound_id: Identifier::vanilla_static("test"),
333            fixed_range: None,
334        };
335        let fixed = SoundEventHolder::Direct {
336            sound_id: Identifier::vanilla_static("test"),
337            fixed_range: Some(4.0),
338        };
339
340        assert_eq!(variable.range(1.0), 16.0);
341        assert_eq!(variable.range(2.0), 32.0);
342        assert_eq!(fixed.range(2.0), 4.0);
343    }
344}