steel_registry/data_components/components/
provides_trim_material.rs1use std::io::{Cursor, Result, Write};
4
5use simdnbt::{FromNbtTag, ToNbtTag};
6use steel_utils::hash::{ComponentHasher, HashComponent};
7use steel_utils::serial::{ReadFrom, WriteTo};
8
9use crate::RegistryHolder;
10use crate::trim_material::{TrimMaterial, TrimMaterialValue};
11
12#[derive(Debug, Clone, PartialEq)]
14pub struct ProvidesTrimMaterial {
15 material: RegistryHolder<TrimMaterial>,
16}
17
18impl ProvidesTrimMaterial {
19 #[must_use]
20 pub const fn new(material: RegistryHolder<TrimMaterial>) -> Self {
21 Self { material }
22 }
23
24 #[must_use]
25 pub const fn material(&self) -> &RegistryHolder<TrimMaterial> {
26 &self.material
27 }
28
29 #[must_use]
30 pub fn value(&self) -> &TrimMaterialValue {
31 self.material.value()
32 }
33}
34
35impl WriteTo for ProvidesTrimMaterial {
36 fn write(&self, writer: &mut impl Write) -> Result<()> {
37 self.material.write(writer)
38 }
39}
40
41impl ReadFrom for ProvidesTrimMaterial {
42 fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
43 RegistryHolder::read(data).map(Self::new)
44 }
45}
46
47impl ToNbtTag for ProvidesTrimMaterial {
48 fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
49 self.material.to_nbt_tag()
50 }
51}
52
53impl FromNbtTag for ProvidesTrimMaterial {
54 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
55 RegistryHolder::from_nbt_tag(tag).map(Self::new)
56 }
57}
58
59impl HashComponent for ProvidesTrimMaterial {
60 fn hash_component(&self, hasher: &mut ComponentHasher) {
61 self.material.hash_component(hasher);
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use std::io::Cursor;
68
69 use rustc_hash::FxHashMap;
70 use simdnbt::borrow::read_tag;
71 use simdnbt::{FromNbtTag as _, ToNbtTag as _};
72 use steel_utils::Identifier;
73 use steel_utils::codec::VarInt;
74 use steel_utils::hash::HashComponent as _;
75 use steel_utils::serial::{PrefixedWrite as _, ReadFrom as _, WriteTo as _};
76 use text_components::{TextComponent, format::Color};
77
78 use super::ProvidesTrimMaterial;
79 use crate::RegistryHolder;
80 use crate::data_components::vanilla_components::PROVIDES_TRIM_MATERIAL;
81 use crate::init_vanilla_registry;
82 use crate::item_stack::ItemStack;
83 use crate::trim_material::{MaterialAssetGroup, MaterialAssetInfo, TrimMaterialValue};
84 use crate::{REGISTRY, vanilla_items, vanilla_trim_materials};
85
86 fn parse_component(tag: simdnbt::owned::NbtTag) -> Option<ProvidesTrimMaterial> {
87 let mut bytes = Vec::new();
88 tag.write(&mut bytes);
89 let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
90 ProvidesTrimMaterial::from_nbt_tag(borrowed.as_tag())
91 }
92
93 fn inline_component() -> ProvidesTrimMaterial {
94 let mut description = TextComponent::plain("Custom trim material");
95 description.format.color = Some(Color::Rgb(0x12, 0x34, 0x56));
96 let overrides = FxHashMap::from_iter([(
97 Identifier::vanilla_static("iron"),
98 MaterialAssetInfo::new("custom_darker").expect("test suffix should be valid"),
99 )]);
100 ProvidesTrimMaterial::new(RegistryHolder::direct(TrimMaterialValue::new(
101 MaterialAssetGroup::new(
102 MaterialAssetInfo::new("custom").expect("test suffix should be valid"),
103 overrides,
104 ),
105 description,
106 )))
107 }
108
109 #[test]
110 fn registry_reference_round_trips_both_codecs() {
111 init_vanilla_registry();
112 let component =
113 ProvidesTrimMaterial::new(RegistryHolder::reference(&vanilla_trim_materials::IRON));
114
115 let mut network = Vec::new();
116 component
117 .write(&mut network)
118 .expect("registry trim material should encode");
119 assert_eq!(
120 ProvidesTrimMaterial::read(&mut Cursor::new(network.as_slice()))
121 .expect("registry trim material should decode"),
122 component
123 );
124
125 let nbt = component.clone().to_nbt_tag();
126 assert_eq!(nbt, simdnbt::owned::NbtTag::String("minecraft:iron".into()));
127 assert_eq!(parse_component(nbt), Some(component));
128 }
129
130 #[test]
131 fn inline_material_round_trips_and_hashes_its_flattened_record() {
132 init_vanilla_registry();
133 let component = inline_component();
134
135 let mut network = Vec::new();
136 component
137 .write(&mut network)
138 .expect("inline trim material should encode");
139 assert_eq!(
140 ProvidesTrimMaterial::read(&mut Cursor::new(network.as_slice()))
141 .expect("inline trim material should decode"),
142 component
143 );
144
145 let nbt = component.clone().to_nbt_tag();
146 assert_eq!(parse_component(nbt.clone()), Some(component.clone()));
147 assert_eq!(component.compute_hash(), nbt.compute_hash());
148
149 let simdnbt::owned::NbtTag::Compound(compound) = nbt else {
150 panic!("inline trim material should encode as a compound");
151 };
152 assert_eq!(
153 compound
154 .compound("description")
155 .and_then(|description| description.string("color"))
156 .map(|color| color.to_str().into_owned()),
157 Some("#123456".to_owned())
158 );
159 }
160
161 #[test]
162 fn invalid_asset_suffixes_are_rejected_by_both_codecs() {
163 init_vanilla_registry();
164 assert!(MaterialAssetInfo::new("Bad Suffix").is_err());
165
166 let mut network = Vec::new();
167 VarInt(0)
168 .write(&mut network)
169 .expect("direct holder discriminator should encode");
170 "Bad Suffix"
171 .write_prefixed::<VarInt>(&mut network)
172 .expect("invalid test suffix should encode as a string");
173 VarInt(0)
174 .write(&mut network)
175 .expect("empty override map should encode");
176 TextComponent::plain("Invalid material")
177 .write(&mut network)
178 .expect("description should encode");
179 assert!(ProvidesTrimMaterial::read(&mut Cursor::new(network.as_slice())).is_err());
180
181 let mut invalid = simdnbt::owned::NbtCompound::new();
182 invalid.insert("asset_name", "Bad Suffix");
183 invalid.insert("description", "Invalid material");
184 assert!(parse_component(simdnbt::owned::NbtTag::Compound(invalid)).is_none());
185 }
186
187 #[test]
188 fn extracted_item_prototypes_reference_every_vanilla_trim_material() {
189 init_vanilla_registry();
190 let prototypes = [
191 (
192 &*vanilla_items::REDSTONE,
193 &*vanilla_trim_materials::REDSTONE,
194 ),
195 (&*vanilla_items::DIAMOND, &*vanilla_trim_materials::DIAMOND),
196 (&*vanilla_items::EMERALD, &*vanilla_trim_materials::EMERALD),
197 (
198 &*vanilla_items::LAPIS_LAZULI,
199 &*vanilla_trim_materials::LAPIS,
200 ),
201 (&*vanilla_items::QUARTZ, &*vanilla_trim_materials::QUARTZ),
202 (
203 &*vanilla_items::AMETHYST_SHARD,
204 &*vanilla_trim_materials::AMETHYST,
205 ),
206 (&*vanilla_items::IRON_INGOT, &*vanilla_trim_materials::IRON),
207 (
208 &*vanilla_items::COPPER_INGOT,
209 &*vanilla_trim_materials::COPPER,
210 ),
211 (&*vanilla_items::GOLD_INGOT, &*vanilla_trim_materials::GOLD),
212 (
213 &*vanilla_items::NETHERITE_INGOT,
214 &*vanilla_trim_materials::NETHERITE,
215 ),
216 (
217 &*vanilla_items::RESIN_BRICK,
218 &*vanilla_trim_materials::RESIN,
219 ),
220 ];
221
222 assert_eq!(
223 REGISTRY
224 .items
225 .iter()
226 .filter(|(_, item)| item.components.has(PROVIDES_TRIM_MATERIAL))
227 .count(),
228 prototypes.len()
229 );
230 for (item, material) in prototypes {
231 assert_eq!(
232 ItemStack::new(item)
233 .get(PROVIDES_TRIM_MATERIAL)
234 .and_then(|component| component.material().as_reference()),
235 Some(material)
236 );
237 }
238 }
239}