Skip to main content

steel_registry/
trim_material.rs

1//! Armor trim material registry values.
2
3use 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/// Texture suffix used by an armor trim material.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct MaterialAssetInfo {
20    suffix: String,
21}
22
23impl MaterialAssetInfo {
24    /// Creates an asset suffix accepted by Vanilla's resource-path codec.
25    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/// Invalid value rejected by `ExtraCodecs.RESOURCE_PATH_CODEC`.
66#[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/// Base texture suffix and equipment-asset-specific overrides.
91#[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    /// Resolves the suffix for an equipment asset, falling back to the base.
117    #[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/// Complete registry-independent trim material definition.
179#[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/// Registered armor trim material definition.
305#[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}