1use std::borrow::Cow;
4use std::io::{Cursor, Error, Result, Write};
5
6use rustc_hash::FxHashMap;
7use simdnbt::owned::{NbtCompound, NbtTag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::Identifier;
10use steel_utils::codec::VarInt;
11use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
12use steel_utils::serial::{PrefixedRead, PrefixedWrite, ReadFrom, WriteTo};
13
14use crate::{REGISTRY, RegistryExt, RegistryHolderEntry, RegistryTags};
15
16const MAX_NETWORK_STRING_LENGTH: usize = 32_767;
17const MAX_NETWORK_STRING_BYTES: usize = MAX_NETWORK_STRING_LENGTH * 3;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct BannerPatternValue {
22 asset_id: Identifier,
23 translation_key: Cow<'static, str>,
24}
25
26impl BannerPatternValue {
27 #[must_use]
28 pub const fn new(asset_id: Identifier, translation_key: Cow<'static, str>) -> Self {
29 Self {
30 asset_id,
31 translation_key,
32 }
33 }
34
35 #[must_use]
36 pub const fn asset_id(&self) -> &Identifier {
37 &self.asset_id
38 }
39
40 #[must_use]
41 pub fn translation_key(&self) -> &str {
42 &self.translation_key
43 }
44
45 fn to_nbt_tag_ref(&self) -> NbtTag {
46 let mut compound = NbtCompound::new();
47 compound.insert("asset_id", self.asset_id.clone());
48 compound.insert("translation_key", self.translation_key.as_ref());
49 NbtTag::Compound(compound)
50 }
51}
52
53impl WriteTo for BannerPatternValue {
54 fn write(&self, writer: &mut impl Write) -> Result<()> {
55 self.asset_id.write(writer)?;
56 write_network_string(&self.translation_key, writer)
57 }
58}
59
60impl ReadFrom for BannerPatternValue {
61 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
62 Ok(Self::new(
63 Identifier::read(data)?,
64 Cow::Owned(read_network_string(data)?),
65 ))
66 }
67}
68
69impl ToNbtTag for BannerPatternValue {
70 fn to_nbt_tag(self) -> NbtTag {
71 self.to_nbt_tag_ref()
72 }
73}
74
75impl FromNbtTag for BannerPatternValue {
76 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
77 let compound = tag.compound()?;
78 let asset_id = Identifier::from_nbt_tag(compound.get("asset_id")?)?;
79 let translation_key = compound.get("translation_key")?.string()?.to_str();
80 Some(Self::new(
81 asset_id,
82 Cow::Owned(translation_key.into_owned()),
83 ))
84 }
85}
86
87impl HashComponent for BannerPatternValue {
88 fn hash_component(&self, hasher: &mut ComponentHasher) {
89 let mut entries = Vec::with_capacity(2);
90 push_hash_entry(&mut entries, "asset_id", &self.asset_id);
91 push_hash_entry(
92 &mut entries,
93 "translation_key",
94 self.translation_key.as_ref(),
95 );
96 sort_map_entries(&mut entries);
97 hasher.start_map();
98 for entry in &entries {
99 hasher.put_raw_bytes(&entry.key_bytes);
100 hasher.put_raw_bytes(&entry.value_bytes);
101 }
102 hasher.end_map();
103 }
104}
105
106fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
107 let mut key_hasher = ComponentHasher::new();
108 key.hash_component(&mut key_hasher);
109 let mut value_hasher = ComponentHasher::new();
110 value.hash_component(&mut value_hasher);
111 entries.push(HashEntry::new(key_hasher, value_hasher));
112}
113
114fn write_network_string(value: &str, writer: &mut impl Write) -> Result<()> {
115 if value.encode_utf16().count() > MAX_NETWORK_STRING_LENGTH {
116 return Err(Error::other("String is longer than 32767 UTF-16 units"));
117 }
118 if value.len() > MAX_NETWORK_STRING_BYTES {
119 return Err(Error::other("Encoded string is longer than 98301 bytes"));
120 }
121 value.write_prefixed::<VarInt>(writer)
122}
123
124fn read_network_string(data: &mut Cursor<&[u8]>) -> Result<String> {
125 let value = String::read_prefixed_bound::<VarInt>(data, MAX_NETWORK_STRING_BYTES)?;
126 if value.encode_utf16().count() > MAX_NETWORK_STRING_LENGTH {
127 return Err(Error::other("String is longer than 32767 UTF-16 units"));
128 }
129 Ok(value)
130}
131
132#[derive(Debug)]
134pub struct BannerPattern {
135 pub key: Identifier,
136 value: BannerPatternValue,
137}
138
139impl BannerPattern {
140 #[must_use]
141 pub const fn new(key: Identifier, value: BannerPatternValue) -> Self {
142 Self { key, value }
143 }
144
145 #[must_use]
146 pub const fn value(&self) -> &BannerPatternValue {
147 &self.value
148 }
149}
150
151impl ToNbtTag for &BannerPattern {
152 fn to_nbt_tag(self) -> NbtTag {
153 self.value.to_nbt_tag_ref()
154 }
155}
156
157pub type BannerPatternRef = &'static BannerPattern;
158
159pub struct BannerPatternRegistry {
160 banner_patterns_by_id: Vec<BannerPatternRef>,
161 banner_patterns_by_key: FxHashMap<Identifier, usize>,
162 tags: RegistryTags,
163 allows_registering: bool,
164}
165
166impl BannerPatternRegistry {
167 #[must_use]
168 pub fn new() -> Self {
169 Self {
170 banner_patterns_by_id: Vec::new(),
171 banner_patterns_by_key: FxHashMap::default(),
172 tags: RegistryTags::default(),
173 allows_registering: true,
174 }
175 }
176}
177
178crate::impl_standard_methods!(
179 BannerPatternRegistry,
180 BannerPatternRef,
181 banner_patterns_by_id,
182 banner_patterns_by_key,
183 allows_registering
184);
185
186crate::impl_registry!(
187 BannerPatternRegistry,
188 BannerPattern,
189 banner_patterns_by_id,
190 banner_patterns_by_key,
191 banner_patterns
192);
193
194crate::impl_tagged_registry!(
195 BannerPatternRegistry,
196 banner_patterns_by_key,
197 "banner pattern"
198);
199
200impl RegistryHolderEntry for BannerPattern {
201 type Value = BannerPatternValue;
202
203 const REGISTRY_NAME: &'static str = "banner pattern";
204
205 fn holder_value(&self) -> &Self::Value {
206 &self.value
207 }
208
209 fn holder_by_id(id: usize) -> Option<&'static Self> {
210 REGISTRY.banner_patterns.by_id(id)
211 }
212
213 fn holder_by_key(key: &Identifier) -> Option<&'static Self> {
214 REGISTRY.banner_patterns.by_key(key)
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use std::borrow::Cow;
221 use std::io::Cursor;
222
223 use simdnbt::borrow::read_tag;
224 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
225 use steel_utils::Identifier;
226 use steel_utils::hash::HashComponent as _;
227 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
228
229 use super::BannerPatternValue;
230 use crate::init_vanilla_registry;
231 use crate::{REGISTRY, vanilla_banner_patterns};
232
233 fn parse(tag: simdnbt::owned::NbtTag) -> Option<BannerPatternValue> {
234 let mut bytes = Vec::new();
235 tag.write(&mut bytes);
236 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
237 BannerPatternValue::from_nbt_tag(borrowed.as_tag())
238 }
239
240 #[test]
241 fn generated_patterns_follow_vanilla_registry_order() {
242 init_vanilla_registry();
243 let keys = REGISTRY
244 .banner_patterns
245 .iter()
246 .map(|(_, pattern)| pattern.key.path.as_ref())
247 .collect::<Vec<_>>();
248 assert_eq!(
249 keys,
250 [
251 "base",
252 "square_bottom_left",
253 "square_bottom_right",
254 "square_top_left",
255 "square_top_right",
256 "stripe_bottom",
257 "stripe_top",
258 "stripe_left",
259 "stripe_right",
260 "stripe_center",
261 "stripe_middle",
262 "stripe_downright",
263 "stripe_downleft",
264 "small_stripes",
265 "cross",
266 "straight_cross",
267 "triangle_bottom",
268 "triangle_top",
269 "triangles_bottom",
270 "triangles_top",
271 "diagonal_left",
272 "diagonal_up_right",
273 "diagonal_up_left",
274 "diagonal_right",
275 "circle",
276 "rhombus",
277 "half_vertical",
278 "half_horizontal",
279 "half_vertical_right",
280 "half_horizontal_bottom",
281 "border",
282 "gradient",
283 "gradient_up",
284 "bricks",
285 "curly_border",
286 "globe",
287 "creeper",
288 "skull",
289 "flower",
290 "mojang",
291 "piglin",
292 "flow",
293 "guster",
294 ]
295 );
296 }
297
298 #[test]
299 fn direct_codecs_and_hash_match_vanilla_shape() {
300 init_vanilla_registry();
301 let pattern = vanilla_banner_patterns::BASE.value().clone();
302 let mut network = Vec::new();
303 pattern.write(&mut network).expect("pattern should encode");
304 assert_eq!(
305 BannerPatternValue::read(&mut Cursor::new(network.as_slice()))
306 .expect("pattern should decode"),
307 pattern
308 );
309
310 let nbt = pattern.clone().to_nbt_tag();
311 assert_eq!(parse(nbt.clone()), Some(pattern.clone()));
312 assert_eq!(pattern.compute_hash(), nbt.compute_hash());
313 }
314
315 #[test]
316 fn direct_codecs_reject_invalid_identifiers_and_long_network_strings() {
317 let mut compound = simdnbt::owned::NbtCompound::new();
318 compound.insert("asset_id", "Invalid Asset");
319 compound.insert("translation_key", "block.minecraft.banner.invalid");
320 assert!(parse(simdnbt::owned::NbtTag::Compound(compound)).is_none());
321
322 let too_long = BannerPatternValue::new(
323 Identifier::vanilla_static("base"),
324 Cow::Owned("x".repeat(32_768)),
325 );
326 assert!(too_long.write(&mut Vec::new()).is_err());
327 }
328}