Skip to main content

steel_registry/
trim_pattern.rs

1//! Armor trim pattern registry values.
2
3use 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/// Complete registry-independent trim pattern definition.
17#[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/// Registered armor trim pattern definition.
120#[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        // HashOps preserves Codec.BOOL while NbtOps represents it as a byte.
271        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}