steel_registry/data_components/components/
use_effects.rs1use 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#[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}