Skip to main content

steel_utils/nbt/
value_input.rs

1//! Lenient numeric reads of NBT compounds.
2
3use simdnbt::{
4    borrow::{NbtCompound as BorrowedNbtCompound, NbtTag as BorrowedNbtTag},
5    owned::{NbtCompound as OwnedNbtCompound, NbtTag as OwnedNbtTag},
6};
7
8/// A numeric NBT tag that converts to any numeric type.
9///
10/// Integer conversions floor floating point tags, while the `codec_*` methods
11/// of [`NbtNumeric`](super::NbtNumeric) truncate them. Use this type for
12/// `getIntOr`-style reads and `NbtNumeric` for `Codec.INT`-style reads.
13#[derive(Clone, Copy, Debug, PartialEq)]
14#[expect(missing_docs, reason = "each variant is the NBT tag of that name")]
15pub enum NumericTag {
16    Byte(i8),
17    Short(i16),
18    Int(i32),
19    Long(i64),
20    Float(f32),
21    Double(f64),
22}
23
24impl NumericTag {
25    const fn floor(value: f64) -> i32 {
26        value.floor() as i32
27    }
28
29    /// Keeps the low bits of the integer value, flooring floating point tags first.
30    #[must_use]
31    pub fn byte_value(self) -> i8 {
32        match self {
33            Self::Byte(value) => value,
34            Self::Short(value) => value as i8,
35            Self::Int(value) => value as i8,
36            Self::Long(value) => value as i8,
37            Self::Float(value) => Self::floor(f64::from(value)) as i8,
38            Self::Double(value) => Self::floor(value) as i8,
39        }
40    }
41
42    /// Keeps the low bits of the integer value, flooring floating point tags first.
43    #[must_use]
44    pub fn short_value(self) -> i16 {
45        match self {
46            Self::Byte(value) => i16::from(value),
47            Self::Short(value) => value,
48            Self::Int(value) => value as i16,
49            Self::Long(value) => value as i16,
50            Self::Float(value) => Self::floor(f64::from(value)) as i16,
51            Self::Double(value) => Self::floor(value) as i16,
52        }
53    }
54
55    /// Keeps the low bits of long tags and floors floating point tags.
56    #[must_use]
57    pub fn int_value(self) -> i32 {
58        match self {
59            Self::Byte(value) => i32::from(value),
60            Self::Short(value) => i32::from(value),
61            Self::Int(value) => value,
62            Self::Long(value) => value as i32,
63            Self::Float(value) => Self::floor(f64::from(value)),
64            Self::Double(value) => Self::floor(value),
65        }
66    }
67
68    /// Truncates float tags but floors double tags, as vanilla does.
69    #[must_use]
70    pub fn long_value(self) -> i64 {
71        match self {
72            Self::Byte(value) => i64::from(value),
73            Self::Short(value) => i64::from(value),
74            Self::Int(value) => i64::from(value),
75            Self::Long(value) => value,
76            Self::Float(value) => value as i64,
77            Self::Double(value) => value.floor() as i64,
78        }
79    }
80
81    /// Converts the value to the nearest `f32`.
82    #[must_use]
83    pub fn float_value(self) -> f32 {
84        match self {
85            Self::Byte(value) => f32::from(value),
86            Self::Short(value) => f32::from(value),
87            Self::Int(value) => value as f32,
88            Self::Long(value) => value as f32,
89            Self::Float(value) => value,
90            Self::Double(value) => value as f32,
91        }
92    }
93
94    /// Converts the value to the nearest `f64`.
95    #[must_use]
96    pub fn double_value(self) -> f64 {
97        match self {
98            Self::Byte(value) => f64::from(value),
99            Self::Short(value) => f64::from(value),
100            Self::Int(value) => f64::from(value),
101            Self::Long(value) => value as f64,
102            Self::Float(value) => f64::from(value),
103            Self::Double(value) => value,
104        }
105    }
106}
107
108impl TryFrom<&OwnedNbtTag> for NumericTag {
109    type Error = ();
110
111    fn try_from(tag: &OwnedNbtTag) -> Result<Self, Self::Error> {
112        match tag {
113            OwnedNbtTag::Byte(value) => Ok(Self::Byte(*value)),
114            OwnedNbtTag::Short(value) => Ok(Self::Short(*value)),
115            OwnedNbtTag::Int(value) => Ok(Self::Int(*value)),
116            OwnedNbtTag::Long(value) => Ok(Self::Long(*value)),
117            OwnedNbtTag::Float(value) => Ok(Self::Float(*value)),
118            OwnedNbtTag::Double(value) => Ok(Self::Double(*value)),
119            _ => Err(()),
120        }
121    }
122}
123
124impl TryFrom<&BorrowedNbtTag<'_, '_>> for NumericTag {
125    type Error = ();
126
127    fn try_from(tag: &BorrowedNbtTag<'_, '_>) -> Result<Self, Self::Error> {
128        tag.byte()
129            .map(Self::Byte)
130            .or_else(|| tag.short().map(Self::Short))
131            .or_else(|| tag.int().map(Self::Int))
132            .or_else(|| tag.long().map(Self::Long))
133            .or_else(|| tag.float().map(Self::Float))
134            .or_else(|| tag.double().map(Self::Double))
135            .ok_or(())
136    }
137}
138
139/// Numeric reads accepting any numeric tag, unlike simdnbt's getters which
140/// return `None` when the tag type differs.
141///
142/// `getIntOr(name, default)` is `int_value(name).unwrap_or(default)` here, and
143/// `getBooleanOr(name, false)` is `byte_value(name).is_some_and(|v| v != 0)`.
144pub trait NbtValueInput {
145    /// Returns the tag called `name` if it exists and is numeric.
146    fn numeric_tag(&self, name: &str) -> Option<NumericTag>;
147
148    /// Reads `name` as an `i8`, see [`NumericTag::byte_value`].
149    fn byte_value(&self, name: &str) -> Option<i8> {
150        self.numeric_tag(name).map(NumericTag::byte_value)
151    }
152
153    /// Reads `name` as an `i16`, see [`NumericTag::short_value`].
154    fn short_value(&self, name: &str) -> Option<i16> {
155        self.numeric_tag(name).map(NumericTag::short_value)
156    }
157
158    /// Reads `name` as an `i32`, see [`NumericTag::int_value`].
159    fn int_value(&self, name: &str) -> Option<i32> {
160        self.numeric_tag(name).map(NumericTag::int_value)
161    }
162
163    /// Reads `name` as an `i64`, see [`NumericTag::long_value`].
164    fn long_value(&self, name: &str) -> Option<i64> {
165        self.numeric_tag(name).map(NumericTag::long_value)
166    }
167
168    /// Reads `name` as an `f32`, see [`NumericTag::float_value`].
169    fn float_value(&self, name: &str) -> Option<f32> {
170        self.numeric_tag(name).map(NumericTag::float_value)
171    }
172
173    /// Reads `name` as an `f64`, see [`NumericTag::double_value`].
174    fn double_value(&self, name: &str) -> Option<f64> {
175        self.numeric_tag(name).map(NumericTag::double_value)
176    }
177}
178
179impl NbtValueInput for OwnedNbtCompound {
180    fn numeric_tag(&self, name: &str) -> Option<NumericTag> {
181        self.get(name)?.try_into().ok()
182    }
183}
184
185impl NbtValueInput for BorrowedNbtCompound<'_, '_> {
186    fn numeric_tag(&self, name: &str) -> Option<NumericTag> {
187        (&self.get(name)?).try_into().ok()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use std::io::Cursor;
194
195    use simdnbt::borrow::read_tag;
196    use simdnbt::owned::{NbtCompound, NbtTag};
197
198    use super::{NbtValueInput as _, NumericTag};
199
200    #[test]
201    fn integer_tags_narrow_by_keeping_low_bits() {
202        assert_eq!(NumericTag::Short(0x1FF).byte_value(), -1);
203        assert_eq!(NumericTag::Int(0x1_FFFF).short_value(), -1);
204        assert_eq!(
205            NumericTag::Long(i64::from(i32::MAX) + 1).int_value(),
206            i32::MIN
207        );
208        assert_eq!(NumericTag::Long(0x1FF).byte_value(), -1);
209    }
210
211    #[test]
212    fn floating_tags_floor_into_integers() {
213        assert_eq!(NumericTag::Float(-1.5).int_value(), -2);
214        assert_eq!(NumericTag::Double(-1.5).int_value(), -2);
215        assert_eq!(NumericTag::Float(5.9).int_value(), 5);
216        assert_eq!(NumericTag::Double(-0.5).short_value(), -1);
217        assert_eq!(NumericTag::Float(-0.5).byte_value(), -1);
218    }
219
220    #[test]
221    fn float_long_value_truncates_but_double_floors() {
222        assert_eq!(NumericTag::Float(-1.5).long_value(), -1);
223        assert_eq!(NumericTag::Double(-1.5).long_value(), -2);
224    }
225
226    #[test]
227    fn floating_tags_saturate_before_narrowing() {
228        assert_eq!(NumericTag::Double(1e300).int_value(), i32::MAX);
229        assert_eq!(NumericTag::Double(1e300).byte_value(), -1);
230        assert_eq!(NumericTag::Float(f32::NAN).int_value(), 0);
231    }
232
233    #[test]
234    fn compound_reads_accept_any_numeric_tag() {
235        let mut nbt = NbtCompound::new();
236        nbt.insert("Radius", 1);
237        nbt.insert("Duration", 200i16);
238        nbt.insert("Name", "1");
239
240        assert_eq!(nbt.float_value("Radius"), Some(1.0));
241        assert_eq!(nbt.int_value("Duration"), Some(200));
242        assert_eq!(nbt.int_value("Name"), None);
243        assert_eq!(nbt.int_value("Missing"), None);
244    }
245
246    #[test]
247    fn borrowed_compound_reads_accept_any_numeric_tag() {
248        let mut nbt = NbtCompound::new();
249        nbt.insert("Radius", 1);
250        let mut bytes = Vec::new();
251        NbtTag::Compound(nbt).write(&mut bytes);
252        let borrowed =
253            read_tag(&mut Cursor::new(bytes.as_slice())).expect("owned test tag should parse");
254        let tag = borrowed.as_tag();
255        let compound = tag.compound().expect("test tag is a compound");
256
257        assert_eq!(compound.float_value("Radius"), Some(1.0));
258    }
259}