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#[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 #[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
82unsafe 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 #[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}