Skip to main content

steel_registry/data_components/components/
map_decorations.rs

1//! Vanilla `minecraft:map_decorations` item component.
2
3use std::collections::BTreeMap;
4use std::io::{Cursor, Error, Result, Write};
5use std::str::FromStr;
6
7use simdnbt::owned::{NbtCompound, NbtTag, read_tag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::Identifier;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::nbt::{NbtNumeric as _, vanilla_nbt_heap_size};
12use steel_utils::serial::{ReadFrom, WriteTo};
13
14use crate::map_decoration_type::MapDecorationType;
15use crate::{REGISTRY, RegistryExt, RegistryReference};
16
17const DEFAULT_NBT_QUOTA: u64 = 2_097_152;
18
19/// One named decoration placed on a map.
20#[derive(Debug, Clone)]
21pub struct MapDecorationEntry {
22    decoration_type: RegistryReference<MapDecorationType>,
23    x: f64,
24    z: f64,
25    rotation: f32,
26}
27
28impl PartialEq for MapDecorationEntry {
29    fn eq(&self, other: &Self) -> bool {
30        self.decoration_type == other.decoration_type
31            && java_double_equals(self.x, other.x)
32            && java_double_equals(self.z, other.z)
33            && java_float_equals(self.rotation, other.rotation)
34    }
35}
36
37impl MapDecorationEntry {
38    #[must_use]
39    pub const fn new(
40        decoration_type: RegistryReference<MapDecorationType>,
41        x: f64,
42        z: f64,
43        rotation: f32,
44    ) -> Self {
45        Self {
46            decoration_type,
47            x,
48            z,
49            rotation,
50        }
51    }
52
53    #[must_use]
54    pub const fn decoration_type(&self) -> RegistryReference<MapDecorationType> {
55        self.decoration_type
56    }
57
58    #[must_use]
59    pub const fn x(&self) -> f64 {
60        self.x
61    }
62
63    #[must_use]
64    pub const fn z(&self) -> f64 {
65        self.z
66    }
67
68    #[must_use]
69    pub const fn rotation(&self) -> f32 {
70        self.rotation
71    }
72
73    fn to_nbt_tag_ref(&self) -> NbtTag {
74        let mut compound = NbtCompound::new();
75        compound.insert("type", self.decoration_type.to_nbt_tag());
76        compound.insert("x", self.x);
77        compound.insert("z", self.z);
78        compound.insert("rotation", self.rotation);
79        NbtTag::Compound(compound)
80    }
81
82    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
83        let compound = tag.compound()?;
84        let key = Identifier::from_str(
85            &compound
86                .get("type")?
87                .string()?
88                .to_owned()
89                .try_into_string()
90                .ok()?,
91        )
92        .ok()?;
93        let decoration_type = REGISTRY.map_decoration_types.by_key(&key)?;
94        Some(Self::new(
95            RegistryReference::new(decoration_type),
96            compound.get("x")?.codec_f64()?,
97            compound.get("z")?.codec_f64()?,
98            compound.get("rotation")?.codec_f32()?,
99        ))
100    }
101}
102
103impl HashComponent for MapDecorationEntry {
104    fn hash_component(&self, hasher: &mut ComponentHasher) {
105        let mut entries = Vec::with_capacity(4);
106        push_hash_entry(&mut entries, "type", &self.decoration_type);
107        push_hash_entry(&mut entries, "x", &self.x);
108        push_hash_entry(&mut entries, "z", &self.z);
109        push_hash_entry(&mut entries, "rotation", &self.rotation);
110        hash_entries(hasher, &mut entries);
111    }
112}
113
114/// Decorations keyed by their arbitrary map-local IDs.
115#[derive(Debug, Default, Clone, PartialEq)]
116pub struct MapDecorations {
117    decorations: BTreeMap<String, MapDecorationEntry>,
118}
119
120impl MapDecorations {
121    pub const EMPTY: Self = Self::empty();
122
123    #[must_use]
124    pub const fn empty() -> Self {
125        Self {
126            decorations: BTreeMap::new(),
127        }
128    }
129
130    #[must_use]
131    pub const fn new(decorations: BTreeMap<String, MapDecorationEntry>) -> Self {
132        Self { decorations }
133    }
134
135    #[must_use]
136    pub const fn decorations(&self) -> &BTreeMap<String, MapDecorationEntry> {
137        &self.decorations
138    }
139
140    #[must_use]
141    pub fn is_empty(&self) -> bool {
142        self.decorations.is_empty()
143    }
144
145    /// Mirrors Vanilla's immutable `withDecoration` copy-and-put operation.
146    #[must_use]
147    pub fn with_decoration(&self, id: String, entry: MapDecorationEntry) -> Self {
148        let mut decorations = self.decorations.clone();
149        decorations.insert(id, entry);
150        Self::new(decorations)
151    }
152
153    fn to_nbt_tag_ref(&self) -> NbtTag {
154        let mut compound = NbtCompound::new();
155        for (id, decoration) in &self.decorations {
156            compound.insert(id.as_str(), decoration.to_nbt_tag_ref());
157        }
158        NbtTag::Compound(compound)
159    }
160
161    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
162        let compound = tag.compound()?;
163        let decorations = compound
164            .iter()
165            .map(|(id, entry)| {
166                Some((
167                    id.to_owned().try_into_string().ok()?,
168                    MapDecorationEntry::from_owned_nbt(entry)?,
169                ))
170            })
171            .collect::<Option<BTreeMap<_, _>>>()?;
172        Some(Self::new(decorations))
173    }
174}
175
176impl WriteTo for MapDecorations {
177    fn write(&self, writer: &mut impl Write) -> Result<()> {
178        let mut encoded = Vec::new();
179        self.to_nbt_tag_ref().write(&mut encoded);
180        writer.write_all(&encoded)
181    }
182}
183
184impl ReadFrom for MapDecorations {
185    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
186        let tag =
187            read_tag(data).map_err(|error| Error::other(format!("Invalid NBT: {error:?}")))?;
188        let Some(heap_size) = vanilla_nbt_heap_size(&tag) else {
189            return Err(Error::other("NBT contains malformed modified UTF-8"));
190        };
191        if heap_size > DEFAULT_NBT_QUOTA {
192            return Err(Error::other(format!(
193                "NBT exceeds Vanilla's {DEFAULT_NBT_QUOTA}-byte heap quota"
194            )));
195        }
196        Self::from_owned_nbt(&tag)
197            .ok_or_else(|| Error::other("Map decorations network value is malformed"))
198    }
199}
200
201impl ToNbtTag for MapDecorations {
202    fn to_nbt_tag(self) -> NbtTag {
203        self.to_nbt_tag_ref()
204    }
205}
206
207impl FromNbtTag for MapDecorations {
208    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
209        Self::from_owned_nbt(&tag.to_owned())
210    }
211}
212
213impl HashComponent for MapDecorations {
214    fn hash_component(&self, hasher: &mut ComponentHasher) {
215        let mut entries = Vec::with_capacity(self.decorations.len());
216        for (id, decoration) in &self.decorations {
217            push_hash_entry(&mut entries, id, decoration);
218        }
219        hash_entries(hasher, &mut entries);
220    }
221}
222
223fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
224    let mut key_hasher = ComponentHasher::new();
225    key_hasher.put_string(key);
226    let mut value_hasher = ComponentHasher::new();
227    value.hash_component(&mut value_hasher);
228    entries.push(HashEntry::new(key_hasher, value_hasher));
229}
230
231fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
232    sort_map_entries(entries);
233    hasher.start_map();
234    for entry in entries {
235        hasher.put_raw_bytes(&entry.key_bytes);
236        hasher.put_raw_bytes(&entry.value_bytes);
237    }
238    hasher.end_map();
239}
240
241const fn java_double_equals(left: f64, right: f64) -> bool {
242    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
243}
244
245const fn java_float_equals(left: f32, right: f32) -> bool {
246    (left.is_nan() && right.is_nan()) || left.to_bits() == right.to_bits()
247}
248
249#[cfg(test)]
250mod tests {
251    use std::collections::BTreeMap;
252    use std::io::Cursor;
253
254    use simdnbt::owned::{NbtCompound, NbtTag};
255    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
256    use steel_utils::hash::HashComponent as _;
257    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
258
259    use super::{MapDecorationEntry, MapDecorations};
260    use crate::data_components::vanilla_components::MAP_DECORATIONS;
261    use crate::init_vanilla_registry;
262    use crate::{REGISTRY, RegistryExt, RegistryReference, vanilla_items};
263
264    fn parse(tag: NbtTag) -> Option<MapDecorations> {
265        let mut bytes = Vec::new();
266        tag.write(&mut bytes);
267        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
268        MapDecorations::from_nbt_tag(borrowed.as_tag())
269    }
270
271    #[test]
272    fn decoration_maps_round_trip_codec_derived_network_and_hash() {
273        init_vanilla_registry();
274        let player = REGISTRY
275            .map_decoration_types
276            .by_key(&steel_utils::Identifier::vanilla_static("player"))
277            .expect("player decoration should be registered");
278        let value = MapDecorations::new(BTreeMap::from([(
279            "home".to_owned(),
280            MapDecorationEntry::new(RegistryReference::new(player), 12.5, -3.0, 45.0),
281        )]));
282
283        let mut entry = NbtCompound::new();
284        entry.insert("type", "minecraft:player");
285        entry.insert("x", 12.5_f64);
286        entry.insert("z", -3.0_f64);
287        entry.insert("rotation", 45.0_f32);
288        let mut decorations = NbtCompound::new();
289        decorations.insert("home", entry);
290        let nbt = NbtTag::Compound(decorations);
291
292        assert_eq!(value.clone().to_nbt_tag(), nbt);
293        assert_eq!(parse(nbt.clone()), Some(value.clone()));
294        assert_eq!(value.compute_hash(), nbt.compute_hash());
295
296        let mut network = Vec::new();
297        value
298            .write(&mut network)
299            .expect("decorations should encode");
300        assert_eq!(
301            MapDecorations::read(&mut Cursor::new(network.as_slice()))
302                .expect("decorations should decode"),
303            value
304        );
305    }
306
307    #[test]
308    fn extracted_filled_map_has_empty_decorations() {
309        init_vanilla_registry();
310        let filled_map = REGISTRY
311            .items
312            .by_key(&vanilla_items::FILLED_MAP.key)
313            .expect("filled map should be registered");
314        assert_eq!(
315            filled_map.components.get(MAP_DECORATIONS),
316            Some(MapDecorations::EMPTY)
317        );
318    }
319
320    #[test]
321    fn entry_equality_matches_java_record_float_semantics() {
322        init_vanilla_registry();
323        let player = REGISTRY
324            .map_decoration_types
325            .by_key(&steel_utils::Identifier::vanilla_static("player"))
326            .expect("player decoration should be registered");
327        let player = RegistryReference::new(player);
328
329        assert_eq!(
330            MapDecorationEntry::new(player, f64::NAN, 0.0, f32::NAN),
331            MapDecorationEntry::new(
332                player,
333                f64::from_bits(0x7ff0_0000_0000_0001),
334                0.0,
335                f32::from_bits(0x7f80_0001),
336            )
337        );
338        assert_ne!(
339            MapDecorationEntry::new(player, 0.0, 0.0, 0.0),
340            MapDecorationEntry::new(player, -0.0, 0.0, 0.0)
341        );
342    }
343}