steel_registry/data_components/components/
lodestone_tracker.rs1use std::io::{Cursor, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11use steel_utils::{BlockPos, Identifier};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct GlobalPos {
16 dimension: Identifier,
17 pos: BlockPos,
18}
19
20impl GlobalPos {
21 #[must_use]
22 pub const fn new(dimension: Identifier, pos: BlockPos) -> Self {
23 Self { dimension, pos }
24 }
25
26 #[must_use]
27 pub const fn dimension(&self) -> &Identifier {
28 &self.dimension
29 }
30
31 #[must_use]
32 pub const fn pos(&self) -> BlockPos {
33 self.pos
34 }
35
36 fn to_nbt_tag_ref(&self) -> NbtTag {
37 let mut compound = NbtCompound::new();
38 compound.insert("dimension", self.dimension.to_string());
39 compound.insert(
40 "pos",
41 NbtTag::IntArray(vec![self.pos.x(), self.pos.y(), self.pos.z()]),
42 );
43 NbtTag::Compound(compound)
44 }
45
46 fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
47 let compound = tag.compound()?;
48 let dimension =
49 Identifier::from_str(&compound.get("dimension")?.string()?.to_string()).ok()?;
50 let coordinates = int_stream_from_nbt(compound.get("pos")?)?;
51 let [x, y, z]: [i32; 3] = coordinates.try_into().ok()?;
52 Some(Self::new(dimension, BlockPos::new(x, y, z)))
53 }
54}
55
56impl WriteTo for GlobalPos {
57 fn write(&self, writer: &mut impl Write) -> Result<()> {
58 self.dimension.write(writer)?;
59 self.pos.write(writer)
60 }
61}
62
63impl ReadFrom for GlobalPos {
64 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
65 Ok(Self::new(Identifier::read(data)?, BlockPos::read(data)?))
66 }
67}
68
69impl HashComponent for GlobalPos {
70 fn hash_component(&self, hasher: &mut ComponentHasher) {
71 let mut entries = Vec::with_capacity(2);
72 push_hash_entry(&mut entries, "dimension", &self.dimension);
73 push_hash_entry(&mut entries, "pos", &CodecBlockPos(self.pos));
74 hash_entries(hasher, &mut entries);
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct LodestoneTracker {
81 target: Option<GlobalPos>,
82 tracked: bool,
83}
84
85impl LodestoneTracker {
86 #[must_use]
87 pub const fn new(target: Option<GlobalPos>, tracked: bool) -> Self {
88 Self { target, tracked }
89 }
90
91 #[must_use]
92 pub const fn target(&self) -> Option<&GlobalPos> {
93 self.target.as_ref()
94 }
95
96 #[must_use]
97 pub const fn tracked(&self) -> bool {
98 self.tracked
99 }
100}
101
102impl WriteTo for LodestoneTracker {
103 fn write(&self, writer: &mut impl Write) -> Result<()> {
104 self.target.is_some().write(writer)?;
105 if let Some(target) = &self.target {
106 target.write(writer)?;
107 }
108 self.tracked.write(writer)
109 }
110}
111
112impl ReadFrom for LodestoneTracker {
113 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
114 let target = if bool::read(data)? {
115 Some(GlobalPos::read(data)?)
116 } else {
117 None
118 };
119 Ok(Self::new(target, bool::read(data)?))
120 }
121}
122
123impl ToNbtTag for LodestoneTracker {
124 fn to_nbt_tag(self) -> NbtTag {
125 let mut compound = NbtCompound::new();
126 if let Some(target) = self.target {
127 compound.insert("target", target.to_nbt_tag_ref());
128 }
129 if !self.tracked {
130 compound.insert("tracked", false);
131 }
132 NbtTag::Compound(compound)
133 }
134}
135
136impl FromNbtTag for LodestoneTracker {
137 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
138 let compound = tag.compound()?;
139 let target = match compound.get("target") {
140 Some(tag) => Some(GlobalPos::from_owned_nbt(&tag.to_owned())?),
141 None => None,
142 };
143 let tracked = match compound.get("tracked") {
144 Some(tag) => tag.codec_bool()?,
145 None => true,
146 };
147 Some(Self::new(target, tracked))
148 }
149}
150
151impl HashComponent for LodestoneTracker {
152 fn hash_component(&self, hasher: &mut ComponentHasher) {
153 let mut entries = Vec::with_capacity(2);
154 if let Some(target) = &self.target {
155 push_hash_entry(&mut entries, "target", target);
156 }
157 if !self.tracked {
158 push_hash_entry(&mut entries, "tracked", &false);
159 }
160 hash_entries(hasher, &mut entries);
161 }
162}
163
164struct CodecBlockPos(BlockPos);
165
166impl HashComponent for CodecBlockPos {
167 fn hash_component(&self, hasher: &mut ComponentHasher) {
168 hasher.put_int_array(&[self.0.x(), self.0.y(), self.0.z()]);
169 }
170}
171
172fn int_stream_from_nbt(tag: &NbtTag) -> Option<Vec<i32>> {
173 match tag {
174 NbtTag::IntArray(values) => Some(values.clone()),
175 NbtTag::List(list) => list
176 .as_nbt_tags()
177 .iter()
178 .map(steel_utils::nbt::NbtNumeric::codec_i32)
179 .collect(),
180 _ => None,
181 }
182}
183
184fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
185 let mut key_hasher = ComponentHasher::new();
186 key_hasher.put_string(key);
187 let mut value_hasher = ComponentHasher::new();
188 value.hash_component(&mut value_hasher);
189 entries.push(HashEntry::new(key_hasher, value_hasher));
190}
191
192fn hash_entries(hasher: &mut ComponentHasher, entries: &mut [HashEntry]) {
193 sort_map_entries(entries);
194 hasher.start_map();
195 for entry in entries {
196 hasher.put_raw_bytes(&entry.key_bytes);
197 hasher.put_raw_bytes(&entry.value_bytes);
198 }
199 hasher.end_map();
200}
201
202#[cfg(test)]
203mod tests {
204 use std::io::Cursor;
205
206 use simdnbt::owned::{NbtCompound, NbtTag};
207 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
208 use steel_utils::hash::HashComponent as _;
209 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
210 use steel_utils::{BlockPos, Identifier};
211
212 use super::{GlobalPos, LodestoneTracker};
213
214 fn parse(tag: NbtTag) -> Option<LodestoneTracker> {
215 let mut bytes = Vec::new();
216 tag.write(&mut bytes);
217 let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
218 LodestoneTracker::from_nbt_tag(borrowed.as_tag())
219 }
220
221 #[test]
222 fn lodestone_target_round_trips_and_hashes_through_global_pos_codec() {
223 let tracker = LodestoneTracker::new(
224 Some(GlobalPos::new(
225 Identifier::vanilla_static("overworld"),
226 BlockPos::new(12, -4, 99),
227 )),
228 false,
229 );
230 let nbt = tracker.clone().to_nbt_tag();
231 assert_eq!(parse(nbt.clone()), Some(tracker.clone()));
232 assert_ne!(tracker.compute_hash(), nbt.compute_hash());
234
235 let mut network = Vec::new();
236 tracker.write(&mut network).expect("tracker should encode");
237 assert_eq!(
238 LodestoneTracker::read(&mut Cursor::new(network.as_slice()))
239 .expect("tracker should decode"),
240 tracker
241 );
242 }
243
244 #[test]
245 fn absent_fields_default_to_no_target_and_tracked() {
246 assert_eq!(
247 parse(NbtTag::Compound(NbtCompound::new())),
248 Some(LodestoneTracker::new(None, true))
249 );
250 }
251}