Skip to main content

steel_registry/data_components/components/
armor_trim.rs

1//! Vanilla `minecraft:trim` item component.
2
3use std::io::{Cursor, Result, Write};
4
5use simdnbt::owned::{NbtCompound, NbtTag};
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
8use steel_utils::serial::{ReadFrom, WriteTo};
9
10use crate::RegistryHolder;
11use crate::trim_material::TrimMaterial;
12use crate::trim_pattern::TrimPattern;
13
14/// Material and pattern applied to a trimmed equipment item.
15#[derive(Debug, Clone, PartialEq)]
16pub struct ArmorTrim {
17    material: RegistryHolder<TrimMaterial>,
18    pattern: RegistryHolder<TrimPattern>,
19}
20
21impl ArmorTrim {
22    #[must_use]
23    pub const fn new(
24        material: RegistryHolder<TrimMaterial>,
25        pattern: RegistryHolder<TrimPattern>,
26    ) -> Self {
27        Self { material, pattern }
28    }
29
30    #[must_use]
31    pub const fn material(&self) -> &RegistryHolder<TrimMaterial> {
32        &self.material
33    }
34
35    #[must_use]
36    pub const fn pattern(&self) -> &RegistryHolder<TrimPattern> {
37        &self.pattern
38    }
39}
40
41impl WriteTo for ArmorTrim {
42    fn write(&self, writer: &mut impl Write) -> Result<()> {
43        self.material.write(writer)?;
44        self.pattern.write(writer)
45    }
46}
47
48impl ReadFrom for ArmorTrim {
49    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
50        Ok(Self::new(
51            RegistryHolder::read(data)?,
52            RegistryHolder::read(data)?,
53        ))
54    }
55}
56
57impl ToNbtTag for ArmorTrim {
58    fn to_nbt_tag(self) -> NbtTag {
59        let mut compound = NbtCompound::new();
60        compound.insert("material", self.material.to_nbt_tag());
61        compound.insert("pattern", self.pattern.to_nbt_tag());
62        NbtTag::Compound(compound)
63    }
64}
65
66impl FromNbtTag for ArmorTrim {
67    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
68        let compound = tag.compound()?;
69        Some(Self::new(
70            RegistryHolder::from_nbt_tag(compound.get("material")?)?,
71            RegistryHolder::from_nbt_tag(compound.get("pattern")?)?,
72        ))
73    }
74}
75
76impl HashComponent for ArmorTrim {
77    fn hash_component(&self, hasher: &mut ComponentHasher) {
78        let mut entries = Vec::new();
79        push_hash_entry(&mut entries, "material", &self.material);
80        push_hash_entry(&mut entries, "pattern", &self.pattern);
81        sort_map_entries(&mut entries);
82        hasher.start_map();
83        for entry in &entries {
84            hasher.put_raw_bytes(&entry.key_bytes);
85            hasher.put_raw_bytes(&entry.value_bytes);
86        }
87        hasher.end_map();
88    }
89}
90
91fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
92    let mut key_hasher = ComponentHasher::new();
93    key.hash_component(&mut key_hasher);
94    let mut value_hasher = ComponentHasher::new();
95    value.hash_component(&mut value_hasher);
96    entries.push(HashEntry::new(key_hasher, value_hasher));
97}
98
99#[cfg(test)]
100mod tests {
101    use std::io::Cursor;
102
103    use rustc_hash::FxHashMap;
104    use simdnbt::borrow::read_tag;
105    use simdnbt::{FromNbtTag as _, ToNbtTag as _};
106    use steel_utils::Identifier;
107    use steel_utils::hash::HashComponent as _;
108    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
109    use text_components::TextComponent;
110
111    use super::ArmorTrim;
112    use crate::RegistryHolder;
113    use crate::data_components::vanilla_components::TRIM;
114    use crate::init_vanilla_registry;
115    use crate::trim_material::{MaterialAssetGroup, MaterialAssetInfo, TrimMaterialValue};
116    use crate::trim_pattern::TrimPatternValue;
117    use crate::{REGISTRY, vanilla_trim_materials, vanilla_trim_patterns};
118
119    fn parse(tag: simdnbt::owned::NbtTag) -> Option<ArmorTrim> {
120        let mut bytes = Vec::new();
121        tag.write(&mut bytes);
122        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
123        ArmorTrim::from_nbt_tag(borrowed.as_tag())
124    }
125
126    #[test]
127    fn registry_references_round_trip_both_codecs_and_hash_the_record() {
128        init_vanilla_registry();
129        let trim = ArmorTrim::new(
130            RegistryHolder::reference(&vanilla_trim_materials::IRON),
131            RegistryHolder::reference(&vanilla_trim_patterns::SENTRY),
132        );
133
134        let mut network = Vec::new();
135        trim.write(&mut network).expect("trim should encode");
136        assert_eq!(
137            ArmorTrim::read(&mut Cursor::new(network.as_slice())).expect("trim should decode"),
138            trim
139        );
140
141        let nbt = trim.clone().to_nbt_tag();
142        assert_eq!(parse(nbt.clone()), Some(trim.clone()));
143        assert_eq!(trim.compute_hash(), nbt.compute_hash());
144        let simdnbt::owned::NbtTag::Compound(compound) = nbt else {
145            panic!("trim should encode as a compound");
146        };
147        assert_eq!(
148            compound
149                .string("material")
150                .map(|value| value.to_str().into_owned()),
151            Some("minecraft:iron".to_owned())
152        );
153        assert_eq!(
154            compound
155                .string("pattern")
156                .map(|value| value.to_str().into_owned()),
157            Some("minecraft:sentry".to_owned())
158        );
159    }
160
161    #[test]
162    fn inline_material_and_pattern_round_trip_both_codecs() {
163        init_vanilla_registry();
164        let trim = ArmorTrim::new(
165            RegistryHolder::direct(TrimMaterialValue::new(
166                MaterialAssetGroup::new(
167                    MaterialAssetInfo::new("custom").expect("test suffix should be valid"),
168                    FxHashMap::default(),
169                ),
170                TextComponent::plain("Custom material"),
171            )),
172            RegistryHolder::direct(TrimPatternValue::new(
173                Identifier::vanilla_static("custom"),
174                TextComponent::plain("Custom pattern"),
175                true,
176            )),
177        );
178
179        let mut network = Vec::new();
180        trim.write(&mut network).expect("inline trim should encode");
181        let decoded = ArmorTrim::read(&mut Cursor::new(network.as_slice()))
182            .expect("inline trim should decode");
183        assert_eq!(decoded, trim);
184        assert_eq!(decoded.compute_hash(), trim.compute_hash());
185        let nbt = trim.clone().to_nbt_tag();
186        let parsed = parse(nbt).expect("inline trim NBT should decode");
187        assert_eq!(parsed, trim);
188        assert_eq!(parsed.compute_hash(), trim.compute_hash());
189    }
190
191    #[test]
192    fn extracted_item_prototypes_do_not_define_a_default_trim() {
193        init_vanilla_registry();
194        assert_eq!(
195            REGISTRY
196                .items
197                .iter()
198                .filter(|(_, item)| item.components.has(TRIM))
199                .count(),
200            0
201        );
202    }
203}