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