steel_registry/data_components/components/
armor_trim.rs1use 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#[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}