Skip to main content

steel_registry/data_components/components/
item_lore.rs

1//! Vanilla `minecraft:lore` item component.
2
3use 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/// Error returned when lore exceeds vanilla's 256-line limit.
16#[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/// Item lore plus the vanilla display-styled projection of each line.
35#[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}