schematic_mesher/resolver/
model_resolver.rs

1//! Model inheritance resolution.
2
3use crate::error::{MesherError, Result};
4use crate::resource_pack::{BlockModel, ResourcePack};
5use std::collections::HashMap;
6
7/// Maximum depth for model inheritance to prevent infinite loops.
8const MAX_INHERITANCE_DEPTH: usize = 10;
9
10/// Resolves model inheritance chains.
11pub struct ModelResolver<'a> {
12    pack: &'a ResourcePack,
13    cache: std::cell::RefCell<HashMap<String, BlockModel>>,
14}
15
16impl<'a> ModelResolver<'a> {
17    pub fn new(pack: &'a ResourcePack) -> Self {
18        Self {
19            pack,
20            cache: std::cell::RefCell::new(HashMap::new()),
21        }
22    }
23
24    /// Resolve a model with all inherited properties.
25    pub fn resolve(&self, model_location: &str) -> Result<BlockModel> {
26        // Check cache first
27        if let Some(cached) = self.cache.borrow().get(model_location) {
28            return Ok(cached.clone());
29        }
30
31        let resolved = self.resolve_internal(model_location, 0)?;
32
33        // Cache the result
34        self.cache
35            .borrow_mut()
36            .insert(model_location.to_string(), resolved.clone());
37
38        Ok(resolved)
39    }
40
41    fn resolve_internal(&self, model_location: &str, depth: usize) -> Result<BlockModel> {
42        if depth >= MAX_INHERITANCE_DEPTH {
43            return Err(MesherError::ModelInheritanceTooDeep(
44                model_location.to_string(),
45            ));
46        }
47
48        // Normalize the location
49        let normalized = self.normalize_location(model_location);
50
51        // Get the base model
52        let base_model = self.pack.get_model(&normalized).ok_or_else(|| {
53            MesherError::ModelResolution(format!("Model not found: {}", normalized))
54        })?;
55
56        // If there's no parent, return the model as-is
57        let parent_location = match &base_model.parent {
58            Some(parent) => parent.clone(),
59            None => return Ok(base_model.clone()),
60        };
61
62        // Skip builtin parents (like builtin/generated, builtin/entity)
63        if parent_location.starts_with("builtin/") {
64            return Ok(base_model.clone());
65        }
66
67        // Resolve the parent recursively
68        let parent_model = self.resolve_internal(&parent_location, depth + 1)?;
69
70        // Merge parent into child
71        Ok(self.merge_models(&parent_model, base_model))
72    }
73
74    /// Merge a parent model into a child model.
75    /// Child properties override parent properties.
76    fn merge_models(&self, parent: &BlockModel, child: &BlockModel) -> BlockModel {
77        let mut merged = parent.clone();
78
79        // Merge textures (child overrides parent)
80        for (key, value) in &child.textures {
81            merged.textures.insert(key.clone(), value.clone());
82        }
83
84        // Use child elements if present, otherwise keep parent elements
85        if !child.elements.is_empty() {
86            merged.elements = child.elements.clone();
87        }
88
89        // Use child ambient_occlusion setting
90        merged.ambient_occlusion = child.ambient_occlusion;
91
92        // Clear parent reference (model is now resolved)
93        merged.parent = None;
94
95        merged
96    }
97
98    /// Normalize a model location to full resource path.
99    fn normalize_location(&self, location: &str) -> String {
100        if location.contains(':') {
101            location.to_string()
102        } else {
103            format!("minecraft:{}", location)
104        }
105    }
106
107    /// Fully resolve texture references in a model.
108    /// Resolves chains like #side -> #all -> block/stone.
109    pub fn resolve_textures(&self, model: &BlockModel) -> HashMap<String, String> {
110        let mut resolved = HashMap::new();
111
112        for (key, value) in &model.textures {
113            let final_value = self.resolve_texture_chain(value, &model.textures, 0);
114            resolved.insert(key.clone(), final_value);
115        }
116
117        resolved
118    }
119
120    fn resolve_texture_chain(
121        &self,
122        reference: &str,
123        textures: &HashMap<String, String>,
124        depth: usize,
125    ) -> String {
126        if depth >= 10 || !reference.starts_with('#') {
127            return reference.to_string();
128        }
129
130        let key = &reference[1..];
131        if let Some(value) = textures.get(key) {
132            self.resolve_texture_chain(value, textures, depth + 1)
133        } else {
134            reference.to_string()
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::resource_pack::model::{BlockModel, ModelElement, ModelFace};
143    use crate::types::Direction;
144
145    fn create_test_pack() -> ResourcePack {
146        let mut pack = ResourcePack::new();
147
148        // Add cube_all (parent)
149        let cube_all = BlockModel {
150            parent: Some("block/cube".to_string()),
151            textures: [("particle".to_string(), "#all".to_string())]
152                .into_iter()
153                .collect(),
154            elements: vec![ModelElement {
155                from: [0.0, 0.0, 0.0],
156                to: [16.0, 16.0, 16.0],
157                rotation: None,
158                shade: true,
159                faces: Direction::ALL
160                    .iter()
161                    .map(|d| {
162                        (
163                            *d,
164                            ModelFace {
165                                texture: "#all".to_string(),
166                                uv: None,
167                                cullface: Some(*d),
168                                rotation: 0,
169                                tintindex: -1,
170                            },
171                        )
172                    })
173                    .collect(),
174            }],
175            ..Default::default()
176        };
177        pack.add_model("minecraft", "block/cube_all", cube_all);
178
179        // Add cube (grandparent) - just to test inheritance works
180        let cube = BlockModel {
181            parent: None,
182            ambient_occlusion: true,
183            textures: HashMap::new(),
184            elements: vec![],
185            ..Default::default()
186        };
187        pack.add_model("minecraft", "block/cube", cube);
188
189        // Add stone (child of cube_all)
190        let stone = BlockModel {
191            parent: Some("block/cube_all".to_string()),
192            textures: [("all".to_string(), "block/stone".to_string())]
193                .into_iter()
194                .collect(),
195            elements: vec![],
196            ..Default::default()
197        };
198        pack.add_model("minecraft", "block/stone", stone);
199
200        pack
201    }
202
203    #[test]
204    fn test_resolve_simple_model() {
205        let pack = create_test_pack();
206        let resolver = ModelResolver::new(&pack);
207
208        let model = resolver.resolve("minecraft:block/cube").unwrap();
209        assert!(model.parent.is_none());
210    }
211
212    #[test]
213    fn test_resolve_with_inheritance() {
214        let pack = create_test_pack();
215        let resolver = ModelResolver::new(&pack);
216
217        let model = resolver.resolve("minecraft:block/stone").unwrap();
218
219        // Should have inherited elements from cube_all
220        assert!(!model.elements.is_empty());
221
222        // Should have merged textures
223        assert!(model.textures.contains_key("all"));
224        assert_eq!(model.textures.get("all"), Some(&"block/stone".to_string()));
225    }
226
227    #[test]
228    fn test_resolve_texture_chain() {
229        let pack = create_test_pack();
230        let resolver = ModelResolver::new(&pack);
231
232        let model = resolver.resolve("minecraft:block/stone").unwrap();
233        let resolved_textures = resolver.resolve_textures(&model);
234
235        // particle -> #all -> block/stone
236        assert_eq!(
237            resolved_textures.get("particle"),
238            Some(&"block/stone".to_string())
239        );
240    }
241
242    #[test]
243    fn test_missing_model() {
244        let pack = create_test_pack();
245        let resolver = ModelResolver::new(&pack);
246
247        let result = resolver.resolve("minecraft:block/nonexistent");
248        assert!(result.is_err());
249    }
250}