steel_registry/data_components/components/
sign_text.rs1use std::io::{Cursor, Result, Write};
9
10use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
11use simdnbt::{FromNbtTag, ToNbtTag};
12use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry};
13use steel_utils::serial::{ReadFrom, WriteTo};
14use text_components::TextComponent;
15
16use crate::DyeColor;
17
18pub const SIGN_LINES: usize = 4;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct SignText {
25 pub messages: [TextComponent; SIGN_LINES],
26 pub filtered_messages: Option<[TextComponent; SIGN_LINES]>,
27 pub color: DyeColor,
28 pub has_glowing_text: bool,
29}
30
31impl SignText {
32 #[must_use]
33 pub const fn new(
34 messages: [TextComponent; SIGN_LINES],
35 filtered_messages: Option<[TextComponent; SIGN_LINES]>,
36 color: DyeColor,
37 has_glowing_text: bool,
38 ) -> Self {
39 Self {
40 messages,
41 filtered_messages,
42 color,
43 has_glowing_text,
44 }
45 }
46}
47
48impl WriteTo for SignText {
49 fn write(&self, writer: &mut impl Write) -> Result<()> {
50 self.messages.write(writer)?;
51 self.filtered_messages.write(writer)?;
52 self.color.write(writer)?;
53 self.has_glowing_text.write(writer)
54 }
55}
56
57impl ReadFrom for SignText {
58 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
59 Ok(Self {
60 messages: <[TextComponent; SIGN_LINES]>::read(data)?,
61 filtered_messages: Option::<[TextComponent; SIGN_LINES]>::read(data)?,
62 color: DyeColor::read(data)?,
63 has_glowing_text: bool::read(data)?,
64 })
65 }
66}
67
68fn messages_to_nbt(messages: &[TextComponent; SIGN_LINES]) -> NbtTag {
69 NbtTag::List(NbtList::from(
70 messages
71 .iter()
72 .map(TextComponent::to_codec_nbt)
73 .collect::<Vec<_>>(),
74 ))
75}
76
77fn messages_from_nbt(tag: simdnbt::borrow::NbtTag) -> Option<[TextComponent; SIGN_LINES]> {
78 let tags = tag.list()?.to_owned().as_nbt_tags();
79 let messages = tags
80 .iter()
81 .map(TextComponent::from_nbt)
82 .collect::<Option<Vec<_>>>()?;
83 <[TextComponent; SIGN_LINES]>::try_from(messages).ok()
84}
85
86impl ToNbtTag for SignText {
87 fn to_nbt_tag(self) -> NbtTag {
88 let mut compound = NbtCompound::new();
89 compound.insert("messages", messages_to_nbt(&self.messages));
90 if let Some(filtered) = &self.filtered_messages
93 && filtered != &self.messages
94 {
95 compound.insert("filtered_messages", messages_to_nbt(filtered));
96 }
97 compound.insert("color", self.color.serialized_name());
98 compound.insert("has_glowing_text", self.has_glowing_text);
99 NbtTag::Compound(compound)
100 }
101}
102
103impl FromNbtTag for SignText {
104 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
105 let compound = tag.compound()?;
106 let messages = messages_from_nbt(compound.get("messages")?)?;
107 let filtered_messages = compound
109 .get("filtered_messages")
110 .and_then(messages_from_nbt)
111 .unwrap_or_else(|| messages.clone());
112 let color = compound
113 .get("color")
114 .and_then(DyeColor::from_nbt_tag)
115 .unwrap_or(DyeColor::Black);
116 let has_glowing_text = compound
117 .get("has_glowing_text")
118 .and_then(|tag| tag.byte())
119 .is_some_and(|value| value != 0);
120 Some(Self {
121 messages,
122 filtered_messages: Some(filtered_messages),
123 color,
124 has_glowing_text,
125 })
126 }
127}
128
129impl HashComponent for SignText {
130 fn hash_component(&self, hasher: &mut ComponentHasher) {
131 let mut entries = Vec::with_capacity(4);
132 entries.push(messages_hash_entry("messages", &self.messages));
133 if let Some(filtered) = &self.filtered_messages
136 && filtered != &self.messages
137 {
138 entries.push(messages_hash_entry("filtered_messages", filtered));
139 }
140 push_hash_entry(&mut entries, "color", self.color.serialized_name());
141 push_hash_entry(&mut entries, "has_glowing_text", &self.has_glowing_text);
142
143 hasher.start_map();
144 for entry in entries {
145 hasher.put_raw_bytes(&entry.key_bytes);
146 hasher.put_raw_bytes(&entry.value_bytes);
147 }
148 hasher.end_map();
149 }
150}
151
152fn messages_hash_entry(key: &str, messages: &[TextComponent; SIGN_LINES]) -> HashEntry {
153 let mut key_hasher = ComponentHasher::new();
154 key_hasher.put_string(key);
155 let mut value_hasher = ComponentHasher::new();
156 value_hasher.start_list();
157 for message in messages {
158 message.hash_component(&mut value_hasher);
159 }
160 value_hasher.end_list();
161 HashEntry::new(key_hasher, value_hasher)
162}
163
164fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
165 let mut key_hasher = ComponentHasher::new();
166 key_hasher.put_string(key);
167 let mut value_hasher = ComponentHasher::new();
168 value.hash_component(&mut value_hasher);
169 entries.push(HashEntry::new(key_hasher, value_hasher));
170}
171
172#[cfg(test)]
173mod tests {
174 use std::io::Cursor;
175
176 use simdnbt::FromNbtTag as _;
177 use simdnbt::ToNbtTag as _;
178 use simdnbt::borrow::read_tag;
179 use simdnbt::owned::NbtTag;
180 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
181 use text_components::TextComponent;
182
183 use super::SignText;
184 use crate::DyeColor;
185
186 fn parse(tag: NbtTag) -> Option<SignText> {
187 let mut bytes = Vec::new();
188 tag.write(&mut bytes);
189 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
190 SignText::from_nbt_tag(borrowed.as_tag())
191 }
192
193 fn empty_messages() -> [TextComponent; 4] {
194 std::array::from_fn(|_| TextComponent::plain(""))
195 }
196
197 #[test]
198 fn round_trips_both_codecs_without_filtered_messages() {
199 let value = SignText::new(empty_messages(), None, DyeColor::Black, false);
200
201 let nbt = value.clone().to_nbt_tag();
202 let expected = SignText::new(
204 empty_messages(),
205 Some(empty_messages()),
206 DyeColor::Black,
207 false,
208 );
209 assert_eq!(parse(nbt), Some(expected));
210
211 let mut network = Vec::new();
212 value.write(&mut network).expect("sign text should encode");
213 assert_eq!(
214 SignText::read(&mut Cursor::new(network.as_slice())).expect("sign text should decode"),
215 value
216 );
217 }
218
219 #[test]
220 fn filtered_messages_equal_to_messages_are_not_persisted() {
221 let value = SignText::new(
222 empty_messages(),
223 Some(empty_messages()),
224 DyeColor::Black,
225 false,
226 );
227
228 let nbt = value.clone().to_nbt_tag();
229 let NbtTag::Compound(compound) = &nbt else {
230 panic!("sign text should encode as a compound");
231 };
232 assert!(!compound.contains("filtered_messages"));
233 assert_eq!(parse(nbt), Some(value));
234 }
235
236 #[test]
237 fn round_trips_with_differing_filtered_messages() {
238 let value = SignText::new(
239 empty_messages(),
240 Some(std::array::from_fn(|_| TextComponent::plain("filtered"))),
241 DyeColor::Red,
242 true,
243 );
244
245 let nbt = value.clone().to_nbt_tag();
246 let NbtTag::Compound(compound) = &nbt else {
247 panic!("sign text should encode as a compound");
248 };
249 assert!(compound.contains("filtered_messages"));
250 assert_eq!(parse(nbt), Some(value.clone()));
251
252 let mut network = Vec::new();
253 value.write(&mut network).expect("sign text should encode");
254 assert_eq!(
255 SignText::read(&mut Cursor::new(network.as_slice())).expect("sign text should decode"),
256 value
257 );
258 }
259
260 #[test]
261 fn persistent_codec_defaults_color_and_glow_when_absent() {
262 let mut compound = simdnbt::owned::NbtCompound::new();
263 compound.insert(
264 "messages",
265 simdnbt::owned::NbtList::String(vec!["".into(), "".into(), "".into(), "".into()]),
266 );
267 let parsed = parse(NbtTag::Compound(compound)).expect("minimal sign text should decode");
268 assert_eq!(parsed.color, DyeColor::Black);
269 assert!(!parsed.has_glowing_text);
270 }
271}