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