Skip to main content

steel_registry/data_components/components/
use_effects.rs

1//! Vanilla `minecraft:use_effects` item component.
2
3use std::io::{Cursor, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
8use steel_utils::nbt::NbtNumeric as _;
9use steel_utils::serial::{ReadFrom, WriteTo};
10
11/// Controls movement and vibration behavior while an item is being used.
12#[derive(Debug, Clone, Copy)]
13pub struct UseEffects {
14    pub can_sprint: bool,
15    pub interact_vibrations: bool,
16    pub speed_multiplier: f32,
17}
18
19impl PartialEq for UseEffects {
20    fn eq(&self, other: &Self) -> bool {
21        self.can_sprint == other.can_sprint
22            && self.interact_vibrations == other.interact_vibrations
23            && java_float_equals(self.speed_multiplier, other.speed_multiplier)
24    }
25}
26
27impl UseEffects {
28    pub const DEFAULT: Self = Self {
29        can_sprint: false,
30        interact_vibrations: true,
31        speed_multiplier: 0.2,
32    };
33
34    #[must_use]
35    pub const fn new(can_sprint: bool, interact_vibrations: bool, speed_multiplier: f32) -> Self {
36        Self {
37            can_sprint,
38            interact_vibrations,
39            speed_multiplier,
40        }
41    }
42}
43
44impl Default for UseEffects {
45    fn default() -> Self {
46        Self::DEFAULT
47    }
48}
49
50impl WriteTo for UseEffects {
51    fn write(&self, writer: &mut impl Write) -> Result<()> {
52        self.can_sprint.write(writer)?;
53        self.interact_vibrations.write(writer)?;
54        self.speed_multiplier.write(writer)
55    }
56}
57
58impl ReadFrom for UseEffects {
59    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
60        Ok(Self {
61            can_sprint: bool::read(data)?,
62            interact_vibrations: bool::read(data)?,
63            speed_multiplier: f32::read(data)?,
64        })
65    }
66}
67
68impl ToNbtTag for UseEffects {
69    fn to_nbt_tag(self) -> NbtTag {
70        let mut compound = NbtCompound::new();
71        if self.can_sprint != Self::DEFAULT.can_sprint {
72            compound.insert("can_sprint", self.can_sprint);
73        }
74        if self.interact_vibrations != Self::DEFAULT.interact_vibrations {
75            compound.insert("interact_vibrations", self.interact_vibrations);
76        }
77        if self.speed_multiplier.to_bits() != Self::DEFAULT.speed_multiplier.to_bits() {
78            compound.insert("speed_multiplier", self.speed_multiplier);
79        }
80        NbtTag::Compound(compound)
81    }
82}
83
84impl FromNbtTag for UseEffects {
85    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
86        let compound = tag.compound()?;
87        let can_sprint = compound
88            .get("can_sprint")
89            .map_or(Some(Self::DEFAULT.can_sprint), |tag| tag.codec_bool())?;
90        let interact_vibrations = compound
91            .get("interact_vibrations")
92            .map_or(Some(Self::DEFAULT.interact_vibrations), |tag| {
93                tag.codec_bool()
94            })?;
95        let speed_multiplier = compound
96            .get("speed_multiplier")
97            .map_or(Some(Self::DEFAULT.speed_multiplier), |tag| tag.codec_f32())?;
98        if !speed_multiplier.is_finite()
99            || speed_multiplier.is_sign_negative()
100            || speed_multiplier > 1.0
101        {
102            return None;
103        }
104        Some(Self {
105            can_sprint,
106            interact_vibrations,
107            speed_multiplier,
108        })
109    }
110}
111
112impl HashComponent for UseEffects {
113    fn hash_component(&self, hasher: &mut ComponentHasher) {
114        let mut entries = Vec::new();
115        if self.can_sprint != Self::DEFAULT.can_sprint {
116            push_hash_entry(&mut entries, "can_sprint", &self.can_sprint);
117        }
118        if self.interact_vibrations != Self::DEFAULT.interact_vibrations {
119            push_hash_entry(
120                &mut entries,
121                "interact_vibrations",
122                &self.interact_vibrations,
123            );
124        }
125        if self.speed_multiplier.to_bits() != Self::DEFAULT.speed_multiplier.to_bits() {
126            push_hash_entry(&mut entries, "speed_multiplier", &self.speed_multiplier);
127        }
128        sort_map_entries(&mut entries);
129        hasher.start_map();
130        for entry in entries {
131            hasher.put_raw_bytes(&entry.key_bytes);
132            hasher.put_raw_bytes(&entry.value_bytes);
133        }
134        hasher.end_map();
135    }
136}
137
138fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
139    let mut key_hasher = ComponentHasher::new();
140    key_hasher.put_string(key);
141    let mut value_hasher = ComponentHasher::new();
142    value.hash_component(&mut value_hasher);
143    entries.push(HashEntry::new(key_hasher, value_hasher));
144}
145
146const fn java_float_equals(left: f32, right: f32) -> bool {
147    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
148}
149
150#[cfg(test)]
151mod tests {
152    use std::io::Cursor;
153
154    use simdnbt::FromNbtTag;
155    use simdnbt::borrow::read_tag;
156    use simdnbt::owned::{NbtCompound, NbtTag};
157    use steel_utils::serial::ReadFrom as _;
158
159    use super::UseEffects;
160
161    fn parse(tag: NbtTag) -> Option<UseEffects> {
162        let mut bytes = Vec::new();
163        tag.write(&mut bytes);
164        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
165        UseEffects::from_nbt_tag(borrowed.as_tag())
166    }
167
168    #[test]
169    fn empty_compound_uses_vanilla_defaults() {
170        assert_eq!(
171            parse(NbtTag::Compound(NbtCompound::new())),
172            Some(UseEffects::DEFAULT)
173        );
174    }
175
176    #[test]
177    fn persistent_codec_rejects_out_of_range_speed() {
178        for speed in [-0.0_f32, -0.1, 1.1, f32::NAN] {
179            let mut compound = NbtCompound::new();
180            compound.insert("speed_multiplier", speed);
181            assert_eq!(parse(NbtTag::Compound(compound)), None);
182        }
183    }
184
185    #[test]
186    fn equality_uses_java_record_float_semantics() {
187        assert_eq!(
188            UseEffects::new(false, true, f32::from_bits(0x7fc0_0001)),
189            UseEffects::new(false, true, f32::from_bits(0x7fc0_0002))
190        );
191        assert_ne!(
192            UseEffects::new(false, true, 0.0),
193            UseEffects::new(false, true, -0.0)
194        );
195    }
196
197    #[test]
198    fn network_booleans_treat_any_nonzero_byte_as_true() {
199        let encoded = [2, 0, 0x3e, 0x4c, 0xcc, 0xcd];
200        assert_eq!(
201            UseEffects::read(&mut Cursor::new(encoded.as_slice()))
202                .expect("use effects should decode"),
203            UseEffects::new(true, false, 0.2)
204        );
205    }
206}