Skip to main content

steel_registry/data_components/components/
debug_stick_state.rs

1//! Vanilla `minecraft:debug_stick_state` 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::vanilla_nbt_heap_size;
12use steel_utils::serial::{ReadFrom, WriteTo};
13
14use crate::blocks::BlockRef;
15use crate::{REGISTRY, RegistryExt};
16
17const DEFAULT_NBT_QUOTA: u64 = 2_097_152;
18
19/// Selected property for one block in a debug stick.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct DebugStickProperty {
22    block: BlockRef,
23    property: &'static str,
24}
25
26impl DebugStickProperty {
27    #[must_use]
28    pub const fn block(&self) -> BlockRef {
29        self.block
30    }
31
32    #[must_use]
33    pub const fn property(&self) -> &'static str {
34        self.property
35    }
36}
37
38/// Per-block property selections stored by a debug stick.
39#[derive(Debug, Default, Clone, PartialEq, Eq)]
40pub struct DebugStickState {
41    properties: BTreeMap<String, DebugStickProperty>,
42}
43
44impl DebugStickState {
45    #[must_use]
46    pub const fn empty() -> Self {
47        Self {
48            properties: BTreeMap::new(),
49        }
50    }
51
52    pub fn new(entries: impl IntoIterator<Item = (BlockRef, String)>) -> Result<Self> {
53        let mut properties = BTreeMap::new();
54        for (block, name) in entries {
55            let property = block
56                .properties
57                .iter()
58                .find(|property| property.get_name() == name)
59                .ok_or_else(|| Error::other(format!("Block {} has no property {name}", block.key)))?
60                .get_name();
61            properties.insert(
62                block.key.to_string(),
63                DebugStickProperty { block, property },
64            );
65        }
66        Ok(Self { properties })
67    }
68
69    pub fn properties(&self) -> impl Iterator<Item = &DebugStickProperty> {
70        self.properties.values()
71    }
72
73    fn to_nbt_tag_ref(&self) -> NbtTag {
74        let mut compound = NbtCompound::new();
75        for property in self.properties.values() {
76            compound.insert(property.block.key.to_string(), property.property);
77        }
78        NbtTag::Compound(compound)
79    }
80
81    fn from_owned_nbt(tag: &NbtTag) -> Option<Self> {
82        let compound = tag.compound()?;
83        let mut entries = Vec::with_capacity(compound.len());
84        for (key, value) in compound.iter() {
85            let key = Identifier::from_str(&key.to_string()).ok()?;
86            let block = REGISTRY.blocks.by_key(&key)?;
87            entries.push((block, value.string()?.to_string()));
88        }
89        Self::new(entries).ok()
90    }
91}
92
93impl WriteTo for DebugStickState {
94    fn write(&self, writer: &mut impl Write) -> Result<()> {
95        let mut encoded = Vec::new();
96        self.to_nbt_tag_ref().write(&mut encoded);
97        writer.write_all(&encoded)
98    }
99}
100
101impl ReadFrom for DebugStickState {
102    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
103        let tag =
104            read_tag(data).map_err(|error| Error::other(format!("Invalid NBT: {error:?}")))?;
105        let Some(heap_size) = vanilla_nbt_heap_size(&tag) else {
106            return Err(Error::other("NBT contains malformed modified UTF-8"));
107        };
108        if heap_size > DEFAULT_NBT_QUOTA {
109            return Err(Error::other(format!(
110                "NBT exceeds Vanilla's {DEFAULT_NBT_QUOTA}-byte heap quota"
111            )));
112        }
113        Self::from_owned_nbt(&tag)
114            .ok_or_else(|| Error::other("Debug stick state network value is malformed"))
115    }
116}
117
118impl ToNbtTag for DebugStickState {
119    fn to_nbt_tag(self) -> NbtTag {
120        self.to_nbt_tag_ref()
121    }
122}
123
124impl FromNbtTag for DebugStickState {
125    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
126        Self::from_owned_nbt(&tag.to_owned())
127    }
128}
129
130impl HashComponent for DebugStickState {
131    fn hash_component(&self, hasher: &mut ComponentHasher) {
132        let mut entries = Vec::with_capacity(self.properties.len());
133        for property in self.properties.values() {
134            let mut key_hasher = ComponentHasher::new();
135            key_hasher.put_string(&property.block.key.to_string());
136            let mut value_hasher = ComponentHasher::new();
137            value_hasher.put_string(property.property);
138            entries.push(HashEntry::new(key_hasher, value_hasher));
139        }
140        sort_map_entries(&mut entries);
141        hasher.start_map();
142        for entry in &entries {
143            hasher.put_raw_bytes(&entry.key_bytes);
144            hasher.put_raw_bytes(&entry.value_bytes);
145        }
146        hasher.end_map();
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use std::io::Cursor;
153
154    use simdnbt::owned::{NbtCompound, NbtTag};
155    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
156    use steel_utils::hash::HashComponent as _;
157    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
158
159    use super::DebugStickState;
160    use crate::data_components::vanilla_components::DEBUG_STICK_STATE;
161    use crate::init_vanilla_registry;
162    use crate::{REGISTRY, RegistryExt, vanilla_blocks};
163
164    fn parse(tag: NbtTag) -> Option<DebugStickState> {
165        let mut bytes = Vec::new();
166        tag.write(&mut bytes);
167        let borrowed = simdnbt::borrow::read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
168        DebugStickState::from_nbt_tag(borrowed.as_tag())
169    }
170
171    #[test]
172    fn block_property_map_validates_properties_and_uses_codec_derived_network() {
173        init_vanilla_registry();
174        let value = DebugStickState::new([(&vanilla_blocks::REDSTONE_WIRE, "power".to_owned())])
175            .expect("redstone wire has a power property");
176        let mut compound = NbtCompound::new();
177        compound.insert("minecraft:redstone_wire", "power");
178        let nbt = NbtTag::Compound(compound);
179        assert_eq!(value.clone().to_nbt_tag(), nbt);
180        assert_eq!(parse(nbt.clone()), Some(value.clone()));
181        assert_eq!(value.compute_hash(), nbt.compute_hash());
182
183        let mut network = Vec::new();
184        value
185            .write(&mut network)
186            .expect("debug state should encode");
187        assert_eq!(
188            DebugStickState::read(&mut Cursor::new(network.as_slice()))
189                .expect("debug state should decode"),
190            value
191        );
192        assert!(DebugStickState::new([(&vanilla_blocks::STONE, "missing".to_owned())]).is_err());
193    }
194
195    #[test]
196    fn extracted_debug_stick_has_an_empty_state() {
197        init_vanilla_registry();
198        let item = REGISTRY
199            .items
200            .by_key(&steel_utils::Identifier::vanilla_static("debug_stick"))
201            .expect("debug stick should be registered");
202        assert_eq!(
203            item.components.get(DEBUG_STICK_STATE),
204            Some(DebugStickState::empty())
205        );
206    }
207}