1use std::io::{Cursor, Result, Write};
4
5use rustc_hash::FxHashMap;
6use simdnbt::owned::{NbtCompound, NbtTag};
7use simdnbt::{FromNbtTag, ToNbtTag};
8use steel_utils::Identifier;
9use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
10use steel_utils::nbt::NbtNumeric as _;
11use steel_utils::serial::{ReadFrom, WriteTo};
12use text_components::TextComponent;
13
14use crate::{REGISTRY, RegistryExt, RegistryHolderEntry, RegistryTags};
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct TrimPatternValue {
19 asset_id: Identifier,
20 description: TextComponent,
21 decal: bool,
22}
23
24impl TrimPatternValue {
25 #[must_use]
26 pub const fn new(asset_id: Identifier, description: TextComponent, decal: bool) -> Self {
27 Self {
28 asset_id,
29 description,
30 decal,
31 }
32 }
33
34 #[must_use]
35 pub const fn asset_id(&self) -> &Identifier {
36 &self.asset_id
37 }
38
39 #[must_use]
40 pub const fn description(&self) -> &TextComponent {
41 &self.description
42 }
43
44 #[must_use]
45 pub const fn decal(&self) -> bool {
46 self.decal
47 }
48
49 fn to_nbt_tag_ref(&self) -> NbtTag {
50 let mut compound = NbtCompound::new();
51 compound.insert("asset_id", self.asset_id.clone());
52 compound.insert("description", self.description.to_codec_nbt());
53 compound.insert("decal", self.decal);
54 NbtTag::Compound(compound)
55 }
56}
57
58impl WriteTo for TrimPatternValue {
59 fn write(&self, writer: &mut impl Write) -> Result<()> {
60 self.asset_id.write(writer)?;
61 WriteTo::write(&self.description.to_codec_nbt(), writer)?;
62 self.decal.write(writer)
63 }
64}
65
66impl ReadFrom for TrimPatternValue {
67 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
68 Ok(Self::new(
69 Identifier::read(data)?,
70 TextComponent::read(data)?,
71 bool::read(data)?,
72 ))
73 }
74}
75
76impl ToNbtTag for TrimPatternValue {
77 fn to_nbt_tag(self) -> NbtTag {
78 self.to_nbt_tag_ref()
79 }
80}
81
82impl FromNbtTag for TrimPatternValue {
83 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
84 let compound = tag.compound()?;
85 Some(Self::new(
86 Identifier::from_nbt_tag(compound.get("asset_id")?)?,
87 TextComponent::from_nbt(&compound.get("description")?.to_owned())?,
88 compound
89 .get("decal")
90 .map_or(Some(false), |decal| decal.codec_bool())?,
91 ))
92 }
93}
94
95impl HashComponent for TrimPatternValue {
96 fn hash_component(&self, hasher: &mut ComponentHasher) {
97 let mut entries = Vec::new();
98 push_hash_entry(&mut entries, "asset_id", &self.asset_id);
99 push_hash_entry(&mut entries, "description", &self.description);
100 push_hash_entry(&mut entries, "decal", &self.decal);
101 sort_map_entries(&mut entries);
102 hasher.start_map();
103 for entry in &entries {
104 hasher.put_raw_bytes(&entry.key_bytes);
105 hasher.put_raw_bytes(&entry.value_bytes);
106 }
107 hasher.end_map();
108 }
109}
110
111fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
112 let mut key_hasher = ComponentHasher::new();
113 key.hash_component(&mut key_hasher);
114 let mut value_hasher = ComponentHasher::new();
115 value.hash_component(&mut value_hasher);
116 entries.push(HashEntry::new(key_hasher, value_hasher));
117}
118
119#[derive(Debug)]
121pub struct TrimPattern {
122 pub key: Identifier,
123 value: TrimPatternValue,
124}
125
126impl TrimPattern {
127 #[must_use]
128 pub const fn new(key: Identifier, value: TrimPatternValue) -> Self {
129 Self { key, value }
130 }
131
132 #[must_use]
133 pub const fn value(&self) -> &TrimPatternValue {
134 &self.value
135 }
136}
137
138impl ToNbtTag for &TrimPattern {
139 fn to_nbt_tag(self) -> NbtTag {
140 self.value.to_nbt_tag_ref()
141 }
142}
143
144pub type TrimPatternRef = &'static TrimPattern;
145
146pub struct TrimPatternRegistry {
147 trim_patterns_by_id: Vec<TrimPatternRef>,
148 trim_patterns_by_key: FxHashMap<Identifier, usize>,
149 tags: RegistryTags,
150 allows_registering: bool,
151}
152
153impl TrimPatternRegistry {
154 #[must_use]
155 pub fn new() -> Self {
156 Self {
157 trim_patterns_by_id: Vec::new(),
158 trim_patterns_by_key: FxHashMap::default(),
159 tags: RegistryTags::default(),
160 allows_registering: true,
161 }
162 }
163}
164
165crate::impl_standard_methods!(
166 TrimPatternRegistry,
167 TrimPatternRef,
168 trim_patterns_by_id,
169 trim_patterns_by_key,
170 allows_registering
171);
172
173crate::impl_registry!(
174 TrimPatternRegistry,
175 TrimPattern,
176 trim_patterns_by_id,
177 trim_patterns_by_key,
178 trim_patterns
179);
180crate::impl_tagged_registry!(TrimPatternRegistry, trim_patterns_by_key, "trim pattern");
181
182impl RegistryHolderEntry for TrimPattern {
183 type Value = TrimPatternValue;
184
185 const REGISTRY_NAME: &'static str = "trim pattern";
186
187 fn holder_value(&self) -> &Self::Value {
188 &self.value
189 }
190
191 fn holder_by_id(id: usize) -> Option<&'static Self> {
192 REGISTRY.trim_patterns.by_id(id)
193 }
194
195 fn holder_by_key(key: &Identifier) -> Option<&'static Self> {
196 REGISTRY.trim_patterns.by_key(key)
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use std::io::Cursor;
203
204 use simdnbt::borrow::read_tag;
205 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
206 use steel_utils::Identifier;
207 use steel_utils::hash::HashComponent as _;
208 use steel_utils::serial::{ReadFrom as _, WriteTo as _};
209 use text_components::TextComponent;
210
211 use super::TrimPatternValue;
212 use crate::init_vanilla_registry;
213 use crate::{REGISTRY, vanilla_trim_patterns};
214
215 fn parse(tag: simdnbt::owned::NbtTag) -> Option<TrimPatternValue> {
216 let mut bytes = Vec::new();
217 tag.write(&mut bytes);
218 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
219 TrimPatternValue::from_nbt_tag(borrowed.as_tag())
220 }
221
222 #[test]
223 fn generated_patterns_follow_vanilla_registry_order() {
224 init_vanilla_registry();
225 let keys = REGISTRY
226 .trim_patterns
227 .iter()
228 .map(|(_, pattern)| pattern.key.path.as_ref())
229 .collect::<Vec<_>>();
230 assert_eq!(
231 keys,
232 [
233 "sentry",
234 "dune",
235 "coast",
236 "wild",
237 "ward",
238 "eye",
239 "vex",
240 "tide",
241 "snout",
242 "rib",
243 "spire",
244 "wayfinder",
245 "shaper",
246 "silence",
247 "raiser",
248 "host",
249 "flow",
250 "bolt",
251 ]
252 );
253 }
254
255 #[test]
256 fn direct_codecs_always_encode_decal_and_default_it_when_absent() {
257 init_vanilla_registry();
258 let pattern = vanilla_trim_patterns::SENTRY.value().clone();
259
260 let mut network = Vec::new();
261 pattern.write(&mut network).expect("pattern should encode");
262 assert_eq!(
263 TrimPatternValue::read(&mut Cursor::new(network.as_slice()))
264 .expect("pattern should decode"),
265 pattern
266 );
267
268 let nbt = pattern.clone().to_nbt_tag();
269 assert_eq!(parse(nbt.clone()), Some(pattern.clone()));
270 assert_ne!(pattern.compute_hash(), nbt.compute_hash());
272 let simdnbt::owned::NbtTag::Compound(mut compound) = nbt else {
273 panic!("pattern should encode as a compound");
274 };
275 assert_eq!(compound.byte("decal"), Some(0));
276 compound.remove("decal");
277 assert_eq!(
278 parse(simdnbt::owned::NbtTag::Compound(compound)),
279 Some(pattern)
280 );
281 }
282
283 #[test]
284 fn direct_persistent_codec_rejects_invalid_asset_identifiers() {
285 let mut compound = simdnbt::owned::NbtCompound::new();
286 compound.insert("asset_id", "Invalid Asset");
287 compound.insert("description", TextComponent::plain("Invalid").to_nbt_tag());
288 compound.insert("decal", false);
289 assert!(parse(simdnbt::owned::NbtTag::Compound(compound)).is_none());
290
291 assert_eq!(
292 vanilla_trim_patterns::SENTRY.value().asset_id(),
293 &Identifier::vanilla_static("sentry")
294 );
295 }
296}