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