steel_registry/data_components/components/
item_lore.rs1use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5use std::io::{Cursor, Result as IoResult, Write};
6
7use simdnbt::owned::{NbtList, NbtTag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::codec::VarInt;
10use steel_utils::hash::{ComponentHasher, HashComponent};
11use steel_utils::serial::{PrefixedRead, PrefixedWrite, ReadFrom, WriteTo};
12use text_components::TextComponent;
13use text_components::format::{Color, Format};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ItemLoreTooLong {
18 pub lines: usize,
19}
20
21impl Display for ItemLoreTooLong {
22 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
23 write!(
24 formatter,
25 "Got {} lines, but maximum is {}",
26 self.lines,
27 ItemLore::MAX_LINES
28 )
29 }
30}
31
32impl Error for ItemLoreTooLong {}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ItemLore {
37 lines: Vec<TextComponent>,
38 styled_lines: Vec<TextComponent>,
39}
40
41impl ItemLore {
42 pub const MAX_LINES: usize = 256;
43
44 #[must_use]
45 pub const fn empty() -> Self {
46 Self {
47 lines: Vec::new(),
48 styled_lines: Vec::new(),
49 }
50 }
51
52 pub fn new(lines: Vec<TextComponent>) -> Result<Self, ItemLoreTooLong> {
53 if lines.len() > Self::MAX_LINES {
54 return Err(ItemLoreTooLong { lines: lines.len() });
55 }
56 let styled_lines = lines.iter().map(styled_line).collect();
57 Ok(Self {
58 lines,
59 styled_lines,
60 })
61 }
62
63 #[must_use]
64 pub fn lines(&self) -> &[TextComponent] {
65 &self.lines
66 }
67
68 #[must_use]
69 pub fn styled_lines(&self) -> &[TextComponent] {
70 &self.styled_lines
71 }
72
73 pub fn with_line_added(&self, line: TextComponent) -> Result<Self, ItemLoreTooLong> {
74 let mut lines = self.lines.clone();
75 lines.push(line);
76 Self::new(lines)
77 }
78}
79
80impl Default for ItemLore {
81 fn default() -> Self {
82 Self::empty()
83 }
84}
85
86fn styled_line(line: &TextComponent) -> TextComponent {
87 let lore_style = Format::new().color(Color::DarkPurple).italic(true);
88 let mut styled = line.clone();
89 styled.format = styled.format.mix(&lore_style);
90 styled
91}
92
93impl WriteTo for ItemLore {
94 fn write(&self, writer: &mut impl Write) -> IoResult<()> {
95 self.lines
96 .write_prefixed_bound::<VarInt>(writer, Self::MAX_LINES)
97 }
98}
99
100impl ReadFrom for ItemLore {
101 fn read(data: &mut Cursor<&[u8]>) -> IoResult<Self> {
102 let lines = Vec::<TextComponent>::read_prefixed_bound::<VarInt>(data, Self::MAX_LINES)?;
103 Self::new(lines).map_err(std::io::Error::other)
104 }
105}
106
107impl ToNbtTag for ItemLore {
108 fn to_nbt_tag(self) -> NbtTag {
109 NbtTag::List(NbtList::from(
110 self.lines
111 .iter()
112 .map(TextComponent::to_codec_nbt)
113 .collect::<Vec<_>>(),
114 ))
115 }
116}
117
118impl FromNbtTag for ItemLore {
119 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
120 let list = tag.list()?;
121 let tags = list.to_owned().as_nbt_tags();
122 if tags.len() > Self::MAX_LINES {
123 return None;
124 }
125 let lines = tags
126 .iter()
127 .map(TextComponent::from_nbt)
128 .collect::<Option<Vec<_>>>()?;
129 Self::new(lines).ok()
130 }
131}
132
133impl HashComponent for ItemLore {
134 fn hash_component(&self, hasher: &mut ComponentHasher) {
135 hasher.start_list();
136 for line in &self.lines {
137 line.hash_component(hasher);
138 }
139 hasher.end_list();
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use std::io::Cursor;
146
147 use simdnbt::borrow::read_tag;
148 use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
149 use simdnbt::{FromNbtTag, ToNbtTag};
150 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
151 use text_components::TextComponent;
152 use text_components::format::Color;
153
154 use super::ItemLore;
155
156 fn parse(tag: NbtTag) -> Option<ItemLore> {
157 let mut bytes = Vec::new();
158 tag.write(&mut bytes);
159 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
160 ItemLore::from_nbt_tag(borrowed.as_tag())
161 }
162
163 #[test]
164 fn styled_lines_apply_lore_defaults_without_overriding_explicit_style() {
165 let mut explicit = TextComponent::plain("explicit");
166 explicit.format.color = Some(Color::Aqua);
167 explicit.format.italic = Some(false);
168 let lore = ItemLore::new(vec![TextComponent::plain("default"), explicit])
169 .expect("two lore lines should fit");
170
171 assert_eq!(lore.styled_lines()[0].format.color, Some(Color::DarkPurple));
172 assert_eq!(lore.styled_lines()[0].format.italic, Some(true));
173 assert_eq!(lore.styled_lines()[1].format.color, Some(Color::Aqua));
174 assert_eq!(lore.styled_lines()[1].format.italic, Some(false));
175 }
176
177 #[test]
178 fn network_codec_round_trips_text_components() {
179 let lore = ItemLore::new(vec![
180 TextComponent::plain("first"),
181 TextComponent::plain("second"),
182 ])
183 .expect("two lore lines should fit");
184 let mut encoded = Vec::new();
185 lore.write(&mut encoded).expect("lore should encode");
186
187 assert_eq!(
188 ItemLore::read(&mut Cursor::new(encoded.as_slice())).expect("lore should decode"),
189 lore
190 );
191 }
192
193 #[test]
194 fn persistent_codec_round_trips_mixed_component_shapes() {
195 let mut complex = NbtCompound::new();
196 complex.insert("text", "second");
197 complex.insert("bold", true);
198 let tag = NbtTag::List(NbtList::from(vec![
199 NbtTag::String("first".into()),
200 NbtTag::Compound(complex),
201 ]));
202 let lore = parse(tag).expect("mixed component list should decode");
203
204 assert_eq!(lore.lines().len(), 2);
205 assert_eq!(parse(lore.clone().to_nbt_tag()), Some(lore));
206 }
207
208 #[test]
209 fn persistent_codec_collapses_plain_lines_to_strings() {
210 let lore = ItemLore::new(vec![
211 TextComponent::plain("first"),
212 TextComponent::plain("second"),
213 ])
214 .expect("two lore lines should fit");
215
216 assert_eq!(
217 lore.to_nbt_tag(),
218 NbtTag::List(NbtList::String(vec!["first".into(), "second".into()]))
219 );
220 }
221}