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