Skip to main content

steel_registry/data_components/components/
tooltip_display.rs

1//! Vanilla `minecraft:tooltip_display` item component.
2
3use std::io::{Cursor, Error, Result, Write};
4use std::str::FromStr;
5
6use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::Identifier;
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
11use steel_utils::nbt::NbtNumeric as _;
12use steel_utils::serial::{ReadFrom, WriteTo};
13
14use crate::data_components::DataComponentType;
15use crate::{REGISTRY, RegistryEntry, RegistryExt};
16
17/// Global tooltip visibility and the ordered set of hidden component types.
18#[derive(Debug, Default, Clone, PartialEq, Eq)]
19pub struct TooltipDisplay {
20    pub hide_tooltip: bool,
21    hidden_components: Vec<Identifier>,
22}
23
24impl TooltipDisplay {
25    pub const DEFAULT: Self = Self {
26        hide_tooltip: false,
27        hidden_components: Vec::new(),
28    };
29
30    #[must_use]
31    pub const fn new(hide_tooltip: bool) -> Self {
32        Self {
33            hide_tooltip,
34            hidden_components: Vec::new(),
35        }
36    }
37
38    #[must_use]
39    pub fn hidden_components(&self) -> &[Identifier] {
40        &self.hidden_components
41    }
42
43    /// Returns a copy with `component` hidden or shown.
44    #[must_use]
45    pub fn with_hidden<T>(&self, component: DataComponentType<T>, hidden: bool) -> Self {
46        self.with_hidden_key(component.key, hidden)
47    }
48
49    #[must_use]
50    pub(crate) fn with_hidden_key(&self, component: Identifier, hidden: bool) -> Self {
51        let mut result = self.clone();
52        let index = result
53            .hidden_components
54            .iter()
55            .position(|key| key == &component);
56        match (index, hidden) {
57            (None, true) => result.hidden_components.push(component),
58            (Some(index), false) => {
59                result.hidden_components.remove(index);
60            }
61            _ => {}
62        }
63        result
64    }
65
66    #[must_use]
67    pub fn shows<T>(&self, component: DataComponentType<T>) -> bool {
68        !self.hide_tooltip && !self.hidden_components.contains(&component.key)
69    }
70
71    fn from_hidden_components(
72        hide_tooltip: bool,
73        components: impl IntoIterator<Item = Identifier>,
74    ) -> Self {
75        let mut hidden_components = Vec::new();
76        for component in components {
77            if !hidden_components.contains(&component) {
78                hidden_components.push(component);
79            }
80        }
81        Self {
82            hide_tooltip,
83            hidden_components,
84        }
85    }
86}
87
88impl WriteTo for TooltipDisplay {
89    fn write(&self, writer: &mut impl Write) -> Result<()> {
90        self.hide_tooltip.write(writer)?;
91        let count = i32::try_from(self.hidden_components.len())
92            .map_err(|_| Error::other("Too many hidden tooltip components"))?;
93        VarInt(count).write(writer)?;
94        for component in &self.hidden_components {
95            let entry = REGISTRY
96                .data_components
97                .by_key(component)
98                .ok_or_else(|| Error::other(format!("Unknown data component type: {component}")))?;
99            let id = entry.try_id().ok_or_else(|| {
100                Error::other(format!("Unregistered data component type: {component}"))
101            })?;
102            let id = i32::try_from(id)
103                .map_err(|_| Error::other(format!("Data component ID is too large: {id}")))?;
104            VarInt(id).write(writer)?;
105        }
106        Ok(())
107    }
108}
109
110impl ReadFrom for TooltipDisplay {
111    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
112        let hide_tooltip = bool::read(data)?;
113        let count = usize::try_from(VarInt::read(data)?.0)
114            .map_err(|_| Error::other("Negative hidden tooltip component count"))?;
115        let mut hidden_components = Vec::with_capacity(count.min(65_536));
116        for _ in 0..count {
117            let id = usize::try_from(VarInt::read(data)?.0)
118                .map_err(|_| Error::other("Negative data component ID"))?;
119            let component = REGISTRY
120                .data_components
121                .by_id(id)
122                .ok_or_else(|| Error::other(format!("Unknown data component ID: {id}")))?;
123            if !hidden_components.contains(&component.key) {
124                hidden_components.push(component.key.clone());
125            }
126        }
127        Ok(Self {
128            hide_tooltip,
129            hidden_components,
130        })
131    }
132}
133
134impl ToNbtTag for TooltipDisplay {
135    fn to_nbt_tag(self) -> NbtTag {
136        let mut compound = NbtCompound::new();
137        if self.hide_tooltip {
138            compound.insert("hide_tooltip", true);
139        }
140        if !self.hidden_components.is_empty() {
141            compound.insert(
142                "hidden_components",
143                NbtList::String(
144                    self.hidden_components
145                        .into_iter()
146                        .map(|key| key.to_string().into())
147                        .collect(),
148                ),
149            );
150        }
151        NbtTag::Compound(compound)
152    }
153}
154
155impl FromNbtTag for TooltipDisplay {
156    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
157        let compound = tag.compound()?;
158        let hide_tooltip = compound
159            .get("hide_tooltip")
160            .map_or(Some(false), |tag| tag.codec_bool())?;
161        let Some(tag) = compound.get("hidden_components") else {
162            return Some(Self::new(hide_tooltip));
163        };
164        let list = tag.list()?;
165        if list.empty() {
166            return Some(Self::new(hide_tooltip));
167        }
168        let strings = list.strings()?;
169        if strings.is_empty() {
170            return Some(Self::new(hide_tooltip));
171        }
172        let hidden_components = strings
173            .iter()
174            .map(|value| Identifier::from_str(&value.to_str()).ok())
175            .collect::<Option<Vec<_>>>()?;
176        if hidden_components
177            .iter()
178            .any(|key| REGISTRY.data_components.by_key(key).is_none())
179        {
180            return None;
181        }
182        Some(Self::from_hidden_components(
183            hide_tooltip,
184            hidden_components,
185        ))
186    }
187}
188
189impl HashComponent for TooltipDisplay {
190    fn hash_component(&self, hasher: &mut ComponentHasher) {
191        let mut entries = Vec::new();
192        if self.hide_tooltip {
193            push_hash_entry(&mut entries, "hide_tooltip", &true);
194        }
195        if !self.hidden_components.is_empty() {
196            let mut key_hasher = ComponentHasher::new();
197            key_hasher.put_string("hidden_components");
198            let mut value_hasher = ComponentHasher::new();
199            value_hasher.start_list();
200            for component in &self.hidden_components {
201                value_hasher.put_string(&component.to_string());
202            }
203            value_hasher.end_list();
204            entries.push(HashEntry::new(key_hasher, value_hasher));
205        }
206        sort_map_entries(&mut entries);
207        hasher.start_map();
208        for entry in entries {
209            hasher.put_raw_bytes(&entry.key_bytes);
210            hasher.put_raw_bytes(&entry.value_bytes);
211        }
212        hasher.end_map();
213    }
214}
215
216fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
217    let mut key_hasher = ComponentHasher::new();
218    key_hasher.put_string(key);
219    let mut value_hasher = ComponentHasher::new();
220    value.hash_component(&mut value_hasher);
221    entries.push(HashEntry::new(key_hasher, value_hasher));
222}
223
224#[cfg(test)]
225mod tests {
226    use std::io::Cursor;
227
228    use simdnbt::borrow::read_tag;
229    use simdnbt::{FromNbtTag, ToNbtTag};
230    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
231
232    use crate::data_components::vanilla_components::{DAMAGE, ENCHANTMENTS, LORE};
233    use crate::init_vanilla_registry;
234
235    use super::TooltipDisplay;
236
237    #[test]
238    fn hidden_components_preserve_first_insertion_order() {
239        let display = TooltipDisplay::DEFAULT
240            .with_hidden(LORE, true)
241            .with_hidden(DAMAGE, true)
242            .with_hidden(LORE, true);
243
244        assert_eq!(display.hidden_components(), &[LORE.key, DAMAGE.key]);
245        assert!(!display.shows(LORE));
246        assert!(display.shows(ENCHANTMENTS));
247    }
248
249    #[test]
250    fn codecs_round_trip_registered_component_types() {
251        init_vanilla_registry();
252
253        let display = TooltipDisplay::DEFAULT
254            .with_hidden(LORE, true)
255            .with_hidden(DAMAGE, true);
256
257        let mut network = Vec::new();
258        display.write(&mut network).expect("display should encode");
259        assert_eq!(
260            TooltipDisplay::read(&mut Cursor::new(network.as_slice()))
261                .expect("display should decode"),
262            display
263        );
264
265        let tag = display.clone().to_nbt_tag();
266        let mut bytes = Vec::new();
267        tag.write(&mut bytes);
268        let borrowed =
269            read_tag(&mut Cursor::new(bytes.as_slice())).expect("display NBT should parse");
270        assert_eq!(
271            TooltipDisplay::from_nbt_tag(borrowed.as_tag()),
272            Some(display)
273        );
274    }
275}