Skip to main content

steel_registry/data_components/components/
lodestone_tracker.rs

1//! Vanilla `minecraft:lodestone_tracker` item component.
2
3use 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/// A block position paired with a dimension resource key.
14#[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/// Optional lodestone target and whether Vanilla should keep validating it.
79#[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        // HashOps preserves Codec.BOOL while NbtOps represents booleans as bytes.
233        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}