1use std::fmt::{self, Display, Formatter};
4use std::io::{Cursor, Error, Result as IoResult, Write};
5
6use rustc_hash::FxHashMap;
7use simdnbt::owned::{NbtCompound, NbtTag};
8use simdnbt::{FromNbtTag, ToNbtTag};
9use steel_utils::Identifier;
10use steel_utils::codec::VarInt;
11use steel_utils::hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries};
12use steel_utils::serial::{PrefixedRead, PrefixedWrite, ReadFrom, WriteTo};
13use text_components::TextComponent;
14
15use crate::{REGISTRY, RegistryExt, RegistryHolderEntry, RegistryTags};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct MaterialAssetInfo {
20 suffix: String,
21}
22
23impl MaterialAssetInfo {
24 pub fn new(suffix: impl Into<String>) -> Result<Self, InvalidMaterialAssetInfo> {
26 let suffix = suffix.into();
27 if !Identifier::validate_path(&suffix) {
28 return Err(InvalidMaterialAssetInfo { suffix });
29 }
30 Ok(Self { suffix })
31 }
32
33 pub(crate) fn from_validated_suffix(suffix: String) -> Self {
34 assert!(
35 Identifier::validate_path(&suffix),
36 "generated trim material contains an invalid asset suffix"
37 );
38 Self { suffix }
39 }
40
41 #[must_use]
42 pub fn suffix(&self) -> &str {
43 &self.suffix
44 }
45}
46
47impl WriteTo for MaterialAssetInfo {
48 fn write(&self, writer: &mut impl Write) -> IoResult<()> {
49 self.suffix.write_prefixed::<VarInt>(writer)
50 }
51}
52
53impl ReadFrom for MaterialAssetInfo {
54 fn read(data: &mut Cursor<&[u8]>) -> IoResult<Self> {
55 Self::new(String::read_prefixed::<VarInt>(data)?).map_err(Error::other)
56 }
57}
58
59impl HashComponent for MaterialAssetInfo {
60 fn hash_component(&self, hasher: &mut ComponentHasher) {
61 self.suffix.hash_component(hasher);
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct InvalidMaterialAssetInfo {
68 suffix: String,
69}
70
71impl InvalidMaterialAssetInfo {
72 #[must_use]
73 pub fn suffix(&self) -> &str {
74 &self.suffix
75 }
76}
77
78impl Display for InvalidMaterialAssetInfo {
79 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
80 write!(
81 formatter,
82 "invalid string to use as a resource path element: {}",
83 self.suffix
84 )
85 }
86}
87
88impl std::error::Error for InvalidMaterialAssetInfo {}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct MaterialAssetGroup {
93 base: MaterialAssetInfo,
94 overrides: FxHashMap<Identifier, MaterialAssetInfo>,
95}
96
97impl MaterialAssetGroup {
98 #[must_use]
99 pub const fn new(
100 base: MaterialAssetInfo,
101 overrides: FxHashMap<Identifier, MaterialAssetInfo>,
102 ) -> Self {
103 Self { base, overrides }
104 }
105
106 #[must_use]
107 pub const fn base(&self) -> &MaterialAssetInfo {
108 &self.base
109 }
110
111 #[must_use]
112 pub const fn overrides(&self) -> &FxHashMap<Identifier, MaterialAssetInfo> {
113 &self.overrides
114 }
115
116 #[must_use]
118 pub fn asset_id(&self, equipment_asset: &Identifier) -> &MaterialAssetInfo {
119 self.overrides.get(equipment_asset).unwrap_or(&self.base)
120 }
121
122 fn insert_nbt_fields(&self, compound: &mut NbtCompound) {
123 compound.insert("asset_name", self.base.suffix());
124 if self.overrides.is_empty() {
125 return;
126 }
127
128 let mut overrides = NbtCompound::new();
129 for (equipment_asset, asset) in &self.overrides {
130 overrides.insert(equipment_asset.to_string(), asset.suffix());
131 }
132 compound.insert("override_armor_assets", NbtTag::Compound(overrides));
133 }
134
135 fn push_hash_fields(&self, entries: &mut Vec<HashEntry>) {
136 push_hash_entry(entries, "asset_name", &self.base);
137 if self.overrides.is_empty() {
138 return;
139 }
140
141 let mut value_hasher = ComponentHasher::new();
142 hash_overrides(&self.overrides, &mut value_hasher);
143 push_prehashed_entry(entries, "override_armor_assets", value_hasher);
144 }
145}
146
147impl WriteTo for MaterialAssetGroup {
148 fn write(&self, writer: &mut impl Write) -> IoResult<()> {
149 self.base.write(writer)?;
150 let count = i32::try_from(self.overrides.len())
151 .map_err(|_| Error::other("trim material override count exceeds protocol range"))?;
152 VarInt(count).write(writer)?;
153 for (equipment_asset, asset) in &self.overrides {
154 equipment_asset.write(writer)?;
155 asset.write(writer)?;
156 }
157 Ok(())
158 }
159}
160
161impl ReadFrom for MaterialAssetGroup {
162 fn read(data: &mut Cursor<&[u8]>) -> IoResult<Self> {
163 let base = MaterialAssetInfo::read(data)?;
164 let encoded_count = VarInt::read(data)?.0;
165 let count = usize::try_from(encoded_count).map_err(|_| {
166 Error::other(format!(
167 "negative trim material override count: {encoded_count}"
168 ))
169 })?;
170 let mut overrides = FxHashMap::default();
171 for _ in 0..count {
172 overrides.insert(Identifier::read(data)?, MaterialAssetInfo::read(data)?);
173 }
174 Ok(Self::new(base, overrides))
175 }
176}
177
178#[derive(Debug, Clone, PartialEq)]
180pub struct TrimMaterialValue {
181 assets: MaterialAssetGroup,
182 description: TextComponent,
183}
184
185impl TrimMaterialValue {
186 #[must_use]
187 pub const fn new(assets: MaterialAssetGroup, description: TextComponent) -> Self {
188 Self {
189 assets,
190 description,
191 }
192 }
193
194 #[must_use]
195 pub const fn assets(&self) -> &MaterialAssetGroup {
196 &self.assets
197 }
198
199 #[must_use]
200 pub const fn description(&self) -> &TextComponent {
201 &self.description
202 }
203
204 fn to_nbt_tag_ref(&self) -> NbtTag {
205 let mut compound = NbtCompound::new();
206 self.assets.insert_nbt_fields(&mut compound);
207 compound.insert("description", self.description.to_codec_nbt());
208 NbtTag::Compound(compound)
209 }
210}
211
212impl WriteTo for TrimMaterialValue {
213 fn write(&self, writer: &mut impl Write) -> IoResult<()> {
214 self.assets.write(writer)?;
215 WriteTo::write(&self.description.to_codec_nbt(), writer)
216 }
217}
218
219impl ReadFrom for TrimMaterialValue {
220 fn read(data: &mut Cursor<&[u8]>) -> IoResult<Self> {
221 Ok(Self::new(
222 MaterialAssetGroup::read(data)?,
223 TextComponent::read(data)?,
224 ))
225 }
226}
227
228impl ToNbtTag for TrimMaterialValue {
229 fn to_nbt_tag(self) -> NbtTag {
230 self.to_nbt_tag_ref()
231 }
232}
233
234impl FromNbtTag for TrimMaterialValue {
235 fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> {
236 let compound = tag.compound()?;
237 let base = MaterialAssetInfo::new(compound.string("asset_name")?.to_str()).ok()?;
238 let mut overrides = FxHashMap::default();
239 if let Some(tag) = compound.get("override_armor_assets") {
240 for (equipment_asset, asset) in tag.compound()?.iter() {
241 overrides.insert(
242 equipment_asset.to_str().parse().ok()?,
243 MaterialAssetInfo::new(asset.string()?.to_str()).ok()?,
244 );
245 }
246 }
247 let description = TextComponent::from_nbt(&compound.get("description")?.to_owned())?;
248 Some(Self::new(
249 MaterialAssetGroup::new(base, overrides),
250 description,
251 ))
252 }
253}
254
255impl HashComponent for TrimMaterialValue {
256 fn hash_component(&self, hasher: &mut ComponentHasher) {
257 let mut entries = Vec::new();
258 self.assets.push_hash_fields(&mut entries);
259 push_hash_entry(&mut entries, "description", &self.description);
260 hash_entries(entries, hasher);
261 }
262}
263
264fn hash_overrides(
265 overrides: &FxHashMap<Identifier, MaterialAssetInfo>,
266 hasher: &mut ComponentHasher,
267) {
268 let mut entries = overrides
269 .iter()
270 .map(|(equipment_asset, asset)| {
271 let mut key_hasher = ComponentHasher::new();
272 equipment_asset.hash_component(&mut key_hasher);
273 let mut value_hasher = ComponentHasher::new();
274 asset.hash_component(&mut value_hasher);
275 HashEntry::new(key_hasher, value_hasher)
276 })
277 .collect::<Vec<_>>();
278 sort_map_entries(&mut entries);
279 hash_entries(entries, hasher);
280}
281
282fn push_hash_entry<T: HashComponent + ?Sized>(entries: &mut Vec<HashEntry>, key: &str, value: &T) {
283 let mut value_hasher = ComponentHasher::new();
284 value.hash_component(&mut value_hasher);
285 push_prehashed_entry(entries, key, value_hasher);
286}
287
288fn push_prehashed_entry(entries: &mut Vec<HashEntry>, key: &str, value_hasher: ComponentHasher) {
289 let mut key_hasher = ComponentHasher::new();
290 key.hash_component(&mut key_hasher);
291 entries.push(HashEntry::new(key_hasher, value_hasher));
292}
293
294fn hash_entries(mut entries: Vec<HashEntry>, hasher: &mut ComponentHasher) {
295 sort_map_entries(&mut entries);
296 hasher.start_map();
297 for entry in &entries {
298 hasher.put_raw_bytes(&entry.key_bytes);
299 hasher.put_raw_bytes(&entry.value_bytes);
300 }
301 hasher.end_map();
302}
303
304#[derive(Debug)]
306pub struct TrimMaterial {
307 pub key: Identifier,
308 value: TrimMaterialValue,
309}
310
311impl TrimMaterial {
312 #[must_use]
313 pub const fn new(key: Identifier, value: TrimMaterialValue) -> Self {
314 Self { key, value }
315 }
316
317 #[must_use]
318 pub const fn value(&self) -> &TrimMaterialValue {
319 &self.value
320 }
321}
322
323impl ToNbtTag for &TrimMaterial {
324 fn to_nbt_tag(self) -> NbtTag {
325 self.value.to_nbt_tag_ref()
326 }
327}
328
329pub type TrimMaterialRef = &'static TrimMaterial;
330
331pub struct TrimMaterialRegistry {
332 trim_materials_by_id: Vec<TrimMaterialRef>,
333 trim_materials_by_key: FxHashMap<Identifier, usize>,
334 tags: RegistryTags,
335 allows_registering: bool,
336}
337
338impl TrimMaterialRegistry {
339 #[must_use]
340 pub fn new() -> Self {
341 Self {
342 trim_materials_by_id: Vec::new(),
343 trim_materials_by_key: FxHashMap::default(),
344 tags: RegistryTags::default(),
345 allows_registering: true,
346 }
347 }
348}
349
350crate::impl_standard_methods!(
351 TrimMaterialRegistry,
352 TrimMaterialRef,
353 trim_materials_by_id,
354 trim_materials_by_key,
355 allows_registering
356);
357
358crate::impl_registry!(
359 TrimMaterialRegistry,
360 TrimMaterial,
361 trim_materials_by_id,
362 trim_materials_by_key,
363 trim_materials
364);
365crate::impl_tagged_registry!(TrimMaterialRegistry, trim_materials_by_key, "trim material");
366
367impl RegistryHolderEntry for TrimMaterial {
368 type Value = TrimMaterialValue;
369
370 const REGISTRY_NAME: &'static str = "trim material";
371
372 fn holder_value(&self) -> &Self::Value {
373 &self.value
374 }
375
376 fn holder_by_id(id: usize) -> Option<&'static Self> {
377 REGISTRY.trim_materials.by_id(id)
378 }
379
380 fn holder_by_key(key: &Identifier) -> Option<&'static Self> {
381 REGISTRY.trim_materials.by_key(key)
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use simdnbt::ToNbtTag as _;
388 use steel_utils::Identifier;
389 use text_components::format::Color;
390
391 use crate::init_vanilla_registry;
392 use crate::{REGISTRY, vanilla_trim_materials};
393
394 #[test]
395 fn generated_materials_follow_vanilla_registry_order_and_asset_groups() {
396 init_vanilla_registry();
397 let keys = REGISTRY
398 .trim_materials
399 .iter()
400 .map(|(_, material)| material.key.path.as_ref())
401 .collect::<Vec<_>>();
402 assert_eq!(
403 keys,
404 [
405 "quartz",
406 "iron",
407 "netherite",
408 "redstone",
409 "copper",
410 "gold",
411 "emerald",
412 "diamond",
413 "lapis",
414 "amethyst",
415 "resin",
416 ]
417 );
418
419 let iron = vanilla_trim_materials::IRON.value();
420 assert_eq!(iron.assets().base().suffix(), "iron");
421 assert_eq!(
422 iron.assets()
423 .asset_id(&Identifier::vanilla_static("iron"))
424 .suffix(),
425 "iron_darker"
426 );
427 assert_eq!(
428 iron.assets()
429 .asset_id(&Identifier::vanilla_static("diamond"))
430 .suffix(),
431 "iron"
432 );
433 assert_eq!(
434 iron.description().format.color,
435 Some(Color::Rgb(0xec, 0xec, 0xec))
436 );
437 }
438
439 #[test]
440 fn generated_definition_uses_the_current_flattened_persistent_shape() {
441 init_vanilla_registry();
442
443 let simdnbt::owned::NbtTag::Compound(iron) = (&*vanilla_trim_materials::IRON).to_nbt_tag()
444 else {
445 panic!("trim material definition should encode as a compound");
446 };
447 assert_eq!(
448 iron.string("asset_name")
449 .map(|value| value.to_str().into_owned()),
450 Some("iron".to_owned())
451 );
452 assert_eq!(
453 iron.compound("override_armor_assets")
454 .and_then(|overrides| overrides.string("minecraft:iron"))
455 .map(|value| value.to_str().into_owned()),
456 Some("iron_darker".to_owned())
457 );
458 assert_eq!(
459 iron.compound("description")
460 .and_then(|description| description.string("color"))
461 .map(|value| value.to_str().into_owned()),
462 Some("#ECECEC".to_owned())
463 );
464
465 let simdnbt::owned::NbtTag::Compound(quartz) =
466 (&*vanilla_trim_materials::QUARTZ).to_nbt_tag()
467 else {
468 panic!("trim material definition should encode as a compound");
469 };
470 assert!(!quartz.contains("override_armor_assets"));
471 }
472}