schematic_mesher/resolver/
model_resolver.rs1use crate::error::{MesherError, Result};
4use crate::resource_pack::{BlockModel, ResourcePack};
5use std::collections::HashMap;
6
7const MAX_INHERITANCE_DEPTH: usize = 10;
9
10pub 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 pub fn resolve(&self, model_location: &str) -> Result<BlockModel> {
26 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 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 let normalized = self.normalize_location(model_location);
50
51 let base_model = self.pack.get_model(&normalized).ok_or_else(|| {
53 MesherError::ModelResolution(format!("Model not found: {}", normalized))
54 })?;
55
56 let parent_location = match &base_model.parent {
58 Some(parent) => parent.clone(),
59 None => return Ok(base_model.clone()),
60 };
61
62 if parent_location.starts_with("builtin/") {
64 return Ok(base_model.clone());
65 }
66
67 let parent_model = self.resolve_internal(&parent_location, depth + 1)?;
69
70 Ok(self.merge_models(&parent_model, base_model))
72 }
73
74 fn merge_models(&self, parent: &BlockModel, child: &BlockModel) -> BlockModel {
77 let mut merged = parent.clone();
78
79 for (key, value) in &child.textures {
81 merged.textures.insert(key.clone(), value.clone());
82 }
83
84 if !child.elements.is_empty() {
86 merged.elements = child.elements.clone();
87 }
88
89 merged.ambient_occlusion = child.ambient_occlusion;
91
92 merged.parent = None;
94
95 merged
96 }
97
98 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 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 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 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 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 assert!(!model.elements.is_empty());
221
222 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 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}