steel_registry/data_components/components/
food.rs1use std::io::{Cursor, Error, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::codec::VarInt;
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11
12#[derive(Debug, Clone)]
14pub struct FoodProperties {
15 nutrition: i32,
16 saturation: f32,
17 can_always_eat: bool,
18}
19
20impl PartialEq for FoodProperties {
21 fn eq(&self, other: &Self) -> bool {
22 self.nutrition == other.nutrition
23 && java_float_equals(self.saturation, other.saturation)
24 && self.can_always_eat == other.can_always_eat
25 }
26}
27
28impl FoodProperties {
29 pub fn new(nutrition: i32, saturation: f32, can_always_eat: bool) -> Result<Self> {
31 if nutrition < 0 {
32 return Err(Error::other("Food nutrition must be non-negative"));
33 }
34 Ok(Self {
35 nutrition,
36 saturation,
37 can_always_eat,
38 })
39 }
40
41 pub(crate) const fn from_extracted(
42 nutrition: i32,
43 saturation: f32,
44 can_always_eat: bool,
45 ) -> Self {
46 assert!(
47 nutrition >= 0,
48 "extracted food nutrition must be non-negative"
49 );
50 Self {
51 nutrition,
52 saturation,
53 can_always_eat,
54 }
55 }
56
57 #[must_use]
58 pub const fn nutrition(&self) -> i32 {
59 self.nutrition
60 }
61
62 #[must_use]
63 pub const fn saturation(&self) -> f32 {
64 self.saturation
65 }
66
67 #[must_use]
68 pub const fn can_always_eat(&self) -> bool {
69 self.can_always_eat
70 }
71}
72
73impl WriteTo for FoodProperties {
74 fn write(&self, writer: &mut impl Write) -> Result<()> {
75 VarInt(self.nutrition).write(writer)?;
76 self.saturation.write(writer)?;
77 self.can_always_eat.write(writer)
78 }
79}
80
81impl ReadFrom for FoodProperties {
82 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
83 Self::new(VarInt::read(data)?.0, f32::read(data)?, bool::read(data)?)
84 }
85}
86
87impl ToNbtTag for FoodProperties {
88 fn to_nbt_tag(self) -> NbtTag {
89 let mut compound = NbtCompound::new();
90 compound.insert("nutrition", self.nutrition);
91 compound.insert("saturation", self.saturation);
92 if self.can_always_eat {
93 compound.insert("can_always_eat", true);
94 }
95 NbtTag::Compound(compound)
96 }
97}
98
99impl FromNbtTag for FoodProperties {
100 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
101 let compound = tag.compound()?;
102 let nutrition = compound.get("nutrition")?.codec_i32()?;
103 let saturation = compound.get("saturation")?.codec_f32()?;
104 let can_always_eat = match compound.get("can_always_eat") {
105 Some(tag) => tag.codec_bool()?,
106 None => false,
107 };
108 Self::new(nutrition, saturation, can_always_eat).ok()
109 }
110}
111
112impl HashComponent for FoodProperties {
113 fn hash_component(&self, hasher: &mut ComponentHasher) {
114 let mut entries = Vec::with_capacity(3);
115 push_hash_entry(&mut entries, "nutrition", &self.nutrition);
116 push_hash_entry(&mut entries, "saturation", &self.saturation);
117 if self.can_always_eat {
118 push_hash_entry(&mut entries, "can_always_eat", &true);
119 }
120 sort_map_entries(&mut entries);
121 hasher.start_map();
122 for entry in &entries {
123 hasher.put_raw_bytes(&entry.key_bytes);
124 hasher.put_raw_bytes(&entry.value_bytes);
125 }
126 hasher.end_map();
127 }
128}
129
130fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
131 let mut key_hasher = ComponentHasher::new();
132 key_hasher.put_string(key);
133 let mut value_hasher = ComponentHasher::new();
134 value.hash_component(&mut value_hasher);
135 entries.push(HashEntry::new(key_hasher, value_hasher));
136}
137
138const fn java_float_equals(left: f32, right: f32) -> bool {
139 (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
140}
141
142#[cfg(test)]
143mod tests {
144 use std::io::Cursor;
145
146 use simdnbt::owned::{NbtCompound, NbtTag};
147 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
148 use steel_utils::hash::HashComponent as _;
149 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
150
151 use super::FoodProperties;
152 use crate::data_components::vanilla_components::FOOD;
153 use crate::init_vanilla_registry;
154 use crate::{REGISTRY, RegistryExt};
155
156 fn parse(tag: NbtTag) -> Option<FoodProperties> {
157 let mut bytes = Vec::new();
158 tag.write(&mut bytes);
159 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
160 FoodProperties::from_nbt_tag(borrowed.as_tag())
161 }
162
163 #[test]
164 fn food_codecs_use_required_values_and_optional_false() {
165 let food = FoodProperties::new(4, 2.4, false).expect("valid food");
166 let mut expected = NbtCompound::new();
167 expected.insert("nutrition", 4);
168 expected.insert("saturation", 2.4_f32);
169 let expected = NbtTag::Compound(expected);
170 assert_eq!(food.clone().to_nbt_tag(), expected);
171 assert_eq!(parse(expected.clone()), Some(food.clone()));
172 assert_eq!(food.compute_hash(), expected.compute_hash());
173
174 let mut network = Vec::new();
175 food.write(&mut network).expect("food should encode");
176 assert_eq!(
177 FoodProperties::read(&mut Cursor::new(network.as_slice())).expect("food should decode"),
178 food
179 );
180 }
181
182 #[test]
183 fn negative_nutrition_is_rejected_for_persistable_values() {
184 assert!(FoodProperties::new(-1, 0.0, false).is_err());
185 let mut invalid = NbtCompound::new();
186 invalid.insert("nutrition", -1);
187 invalid.insert("saturation", 0.0_f32);
188 assert!(parse(NbtTag::Compound(invalid)).is_none());
189 }
190
191 #[test]
192 fn equality_uses_java_record_float_semantics() {
193 assert_eq!(
194 FoodProperties::new(1, f32::from_bits(0x7fc0_0001), false).expect("valid food"),
195 FoodProperties::new(1, f32::from_bits(0x7fc0_0002), false).expect("valid food")
196 );
197 assert_ne!(
198 FoodProperties::new(1, 0.0, false).expect("valid food"),
199 FoodProperties::new(1, -0.0, false).expect("valid food")
200 );
201 }
202
203 #[test]
204 fn extracted_food_prototypes_keep_vanilla_values() {
205 init_vanilla_registry();
206 let apple = REGISTRY
207 .items
208 .by_key(&steel_utils::Identifier::vanilla_static("apple"))
209 .expect("apple should be registered");
210 assert_eq!(
211 apple.components.get(FOOD),
212 Some(FoodProperties::new(4, 2.4, false).expect("valid apple food"))
213 );
214
215 let golden_apple = REGISTRY
216 .items
217 .by_key(&steel_utils::Identifier::vanilla_static("golden_apple"))
218 .expect("golden apple should be registered");
219 assert_eq!(
220 golden_apple.components.get(FOOD),
221 Some(FoodProperties::new(4, 9.6, true).expect("valid golden apple food"))
222 );
223 }
224}