Skip to main content

steel_registry/
instrument.rs

1use std::fmt::{self, Display, Formatter};
2use std::io::{Cursor, Error, Result as IoResult, Write};
3
4use rustc_hash::FxHashMap;
5use simdnbt::owned::{NbtCompound, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::Identifier;
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11use text_components::TextComponent;
12
13use crate::sound_event::SoundEventHolder;
14use crate::{REGISTRY, RegistryExt, RegistryHolderEntry, RegistryTags};
15
16/// A complete instrument definition, either registered or stored inline.
17///
18/// The direct stream codec carries raw floats, but Steel validates them against
19/// the persistent codec before constructing this value so item stacks cannot
20/// contain an instrument that later fails to save.
21#[derive(Debug, Clone)]
22pub struct InstrumentValue {
23    sound_event: SoundEventHolder,
24    use_duration: f32,
25    range: f32,
26    description: TextComponent,
27}
28
29impl InstrumentValue {
30    pub fn new(
31        sound_event: SoundEventHolder,
32        use_duration: f32,
33        range: f32,
34        description: TextComponent,
35    ) -> Result<Self, InvalidInstrumentValue> {
36        if !is_positive_float(use_duration) {
37            return Err(InvalidInstrumentValue::UseDuration(use_duration));
38        }
39        if !is_positive_float(range) {
40            return Err(InvalidInstrumentValue::Range(range));
41        }
42        Ok(Self {
43            sound_event,
44            use_duration,
45            range,
46            description,
47        })
48    }
49
50    pub(crate) const fn from_validated_parts(
51        sound_event: SoundEventHolder,
52        use_duration: f32,
53        range: f32,
54        description: TextComponent,
55    ) -> Self {
56        assert!(is_positive_float(use_duration));
57        assert!(is_positive_float(range));
58        Self {
59            sound_event,
60            use_duration,
61            range,
62            description,
63        }
64    }
65
66    #[must_use]
67    pub const fn sound_event(&self) -> &SoundEventHolder {
68        &self.sound_event
69    }
70
71    #[must_use]
72    pub const fn use_duration(&self) -> f32 {
73        self.use_duration
74    }
75
76    #[must_use]
77    pub const fn range(&self) -> f32 {
78        self.range
79    }
80
81    #[must_use]
82    pub const fn description(&self) -> &TextComponent {
83        &self.description
84    }
85
86    fn to_nbt_tag_ref(&self) -> NbtTag {
87        let mut compound = NbtCompound::new();
88        compound.insert("sound_event", self.sound_event.clone().to_nbt_tag());
89        compound.insert("use_duration", self.use_duration);
90        compound.insert("range", self.range);
91        compound.insert("description", self.description.to_codec_nbt());
92        NbtTag::Compound(compound)
93    }
94}
95
96const fn is_positive_float(value: f32) -> bool {
97    value > 0.0 && value <= f32::MAX
98}
99
100impl PartialEq for InstrumentValue {
101    fn eq(&self, other: &Self) -> bool {
102        self.sound_event == other.sound_event
103            && java_float_equals(self.use_duration, other.use_duration)
104            && java_float_equals(self.range, other.range)
105            && self.description == other.description
106    }
107}
108
109const fn java_float_equals(left: f32, right: f32) -> bool {
110    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
111}
112
113impl WriteTo for InstrumentValue {
114    fn write(&self, writer: &mut impl Write) -> IoResult<()> {
115        self.sound_event.write(writer)?;
116        self.use_duration.write(writer)?;
117        self.range.write(writer)?;
118        WriteTo::write(&self.description.to_codec_nbt(), writer)
119    }
120}
121
122impl ReadFrom for InstrumentValue {
123    fn read(data: &mut Cursor<&[u8]>) -> IoResult<Self> {
124        Self::new(
125            SoundEventHolder::read(data)?,
126            f32::read(data)?,
127            f32::read(data)?,
128            TextComponent::read(data)?,
129        )
130        .map_err(Error::other)
131    }
132}
133
134impl ToNbtTag for InstrumentValue {
135    fn to_nbt_tag(self) -> NbtTag {
136        self.to_nbt_tag_ref()
137    }
138}
139
140impl FromNbtTag for InstrumentValue {
141    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
142        let compound = tag.compound()?;
143        let description = TextComponent::from_nbt(&compound.get("description")?.to_owned())?;
144        Self::new(
145            SoundEventHolder::from_nbt_tag(compound.get("sound_event")?)?,
146            compound.get("use_duration")?.codec_f32()?,
147            compound.get("range")?.codec_f32()?,
148            description,
149        )
150        .ok()
151    }
152}
153
154impl HashComponent for InstrumentValue {
155    fn hash_component(&self, hasher: &mut ComponentHasher) {
156        let mut entries = Vec::new();
157        push_hash_entry(&mut entries, "sound_event", &self.sound_event);
158        push_hash_entry(&mut entries, "use_duration", &self.use_duration);
159        push_hash_entry(&mut entries, "range", &self.range);
160        push_hash_entry(&mut entries, "description", &self.description);
161        sort_map_entries(&mut entries);
162        hasher.start_map();
163        for entry in &entries {
164            hasher.put_raw_bytes(&entry.key_bytes);
165            hasher.put_raw_bytes(&entry.value_bytes);
166        }
167        hasher.end_map();
168    }
169}
170
171fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
172    let mut key_hasher = ComponentHasher::new();
173    key.hash_component(&mut key_hasher);
174    let mut value_hasher = ComponentHasher::new();
175    value.hash_component(&mut value_hasher);
176    entries.push(HashEntry::new(key_hasher, value_hasher));
177}
178
179/// Invalid value rejected by `ExtraCodecs.POSITIVE_FLOAT`.
180#[derive(Debug, Clone, Copy, PartialEq)]
181pub enum InvalidInstrumentValue {
182    UseDuration(f32),
183    Range(f32),
184}
185
186impl Display for InvalidInstrumentValue {
187    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
188        match self {
189            Self::UseDuration(value) => write!(formatter, "use duration must be positive: {value}"),
190            Self::Range(value) => write!(formatter, "range must be positive: {value}"),
191        }
192    }
193}
194
195impl std::error::Error for InvalidInstrumentValue {}
196
197/// Registered instrument definition, primarily used by goat horns.
198#[derive(Debug)]
199pub struct Instrument {
200    pub key: Identifier,
201    value: InstrumentValue,
202}
203
204impl Instrument {
205    #[must_use]
206    pub const fn new(key: Identifier, value: InstrumentValue) -> Self {
207        Self { key, value }
208    }
209
210    #[must_use]
211    pub const fn value(&self) -> &InstrumentValue {
212        &self.value
213    }
214}
215
216impl ToNbtTag for &Instrument {
217    fn to_nbt_tag(self) -> NbtTag {
218        self.value.to_nbt_tag_ref()
219    }
220}
221
222pub type InstrumentRef = &'static Instrument;
223
224pub struct InstrumentRegistry {
225    instruments_by_id: Vec<InstrumentRef>,
226    instruments_by_key: FxHashMap<Identifier, usize>,
227    tags: RegistryTags,
228    allows_registering: bool,
229}
230
231impl InstrumentRegistry {
232    #[must_use]
233    pub fn new() -> Self {
234        Self {
235            instruments_by_id: Vec::new(),
236            instruments_by_key: FxHashMap::default(),
237            tags: RegistryTags::default(),
238            allows_registering: true,
239        }
240    }
241}
242
243crate::impl_standard_methods!(
244    InstrumentRegistry,
245    InstrumentRef,
246    instruments_by_id,
247    instruments_by_key,
248    allows_registering
249);
250
251crate::impl_registry!(
252    InstrumentRegistry,
253    Instrument,
254    instruments_by_id,
255    instruments_by_key,
256    instruments
257);
258
259crate::impl_tagged_registry!(InstrumentRegistry, instruments_by_key, "instrument");
260
261impl RegistryHolderEntry for Instrument {
262    type Value = InstrumentValue;
263
264    const REGISTRY_NAME: &'static str = "instrument";
265
266    fn holder_value(&self) -> &Self::Value {
267        &self.value
268    }
269
270    fn holder_by_id(id: usize) -> Option<&'static Self> {
271        REGISTRY.instruments.by_id(id)
272    }
273
274    fn holder_by_key(key: &Identifier) -> Option<&'static Self> {
275        REGISTRY.instruments.by_key(key)
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use simdnbt::ToNbtTag;
282    use simdnbt::owned::NbtTag;
283
284    use crate::{init_vanilla_registry, vanilla_instruments};
285
286    #[test]
287    fn nbt_uses_sound_event_registry_key() {
288        init_vanilla_registry();
289
290        let NbtTag::Compound(compound) = (&vanilla_instruments::PONDER_GOAT_HORN).to_nbt_tag()
291        else {
292            panic!("instrument did not serialize to a compound tag");
293        };
294
295        let Some(sound_event) = compound.string("sound_event") else {
296            panic!("instrument NBT is missing sound_event string");
297        };
298
299        assert_eq!(
300            sound_event.to_str().as_ref(),
301            "minecraft:item.goat_horn.sound.0"
302        );
303    }
304}