Skip to main content

steel_registry/data_components/components/
food.rs

1//! Vanilla `minecraft:food` item component.
2
3use 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/// Nutrition restored by consuming an item.
13#[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    /// Creates persistable food properties.
30    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}