use crate::error::{MesherError, Result};
use crate::resource_pack::{BlockModel, ResourcePack};
use std::collections::HashMap;
const MAX_INHERITANCE_DEPTH: usize = 10;
pub struct ModelResolver<'a> {
pack: &'a ResourcePack,
cache: std::cell::RefCell<HashMap<String, BlockModel>>,
}
impl<'a> ModelResolver<'a> {
pub fn new(pack: &'a ResourcePack) -> Self {
Self {
pack,
cache: std::cell::RefCell::new(HashMap::new()),
}
}
pub fn resolve(&self, model_location: &str) -> Result<BlockModel> {
if let Some(cached) = self.cache.borrow().get(model_location) {
return Ok(cached.clone());
}
let resolved = self.resolve_internal(model_location, 0)?;
self.cache
.borrow_mut()
.insert(model_location.to_string(), resolved.clone());
Ok(resolved)
}
fn resolve_internal(&self, model_location: &str, depth: usize) -> Result<BlockModel> {
if depth >= MAX_INHERITANCE_DEPTH {
return Err(MesherError::ModelInheritanceTooDeep(
model_location.to_string(),
));
}
let normalized = self.normalize_location(model_location);
let base_model = self.pack.get_model(&normalized).ok_or_else(|| {
MesherError::ModelResolution(format!("Model not found: {}", normalized))
})?;
let parent_location = match &base_model.parent {
Some(parent) => parent.clone(),
None => return Ok(base_model.clone()),
};
if parent_location.starts_with("builtin/") {
return Ok(base_model.clone());
}
let parent_model = self.resolve_internal(&parent_location, depth + 1)?;
Ok(self.merge_models(&parent_model, base_model))
}
fn merge_models(&self, parent: &BlockModel, child: &BlockModel) -> BlockModel {
let mut merged = parent.clone();
for (key, value) in &child.textures {
merged.textures.insert(key.clone(), value.clone());
}
if !child.elements.is_empty() {
merged.elements = child.elements.clone();
}
merged.ambient_occlusion = child.ambient_occlusion;
merged.parent = None;
merged
}
fn normalize_location(&self, location: &str) -> String {
if location.contains(':') {
location.to_string()
} else {
format!("minecraft:{}", location)
}
}
pub fn resolve_textures(&self, model: &BlockModel) -> HashMap<String, String> {
let mut resolved = HashMap::new();
for (key, value) in &model.textures {
let final_value = self.resolve_texture_chain(value, &model.textures, 0);
resolved.insert(key.clone(), final_value);
}
resolved
}
fn resolve_texture_chain(
&self,
reference: &str,
textures: &HashMap<String, String>,
depth: usize,
) -> String {
if depth >= 10 || !reference.starts_with('#') {
return reference.to_string();
}
let key = &reference[1..];
if let Some(value) = textures.get(key) {
self.resolve_texture_chain(value, textures, depth + 1)
} else {
reference.to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resource_pack::model::{BlockModel, ModelElement, ModelFace};
use crate::types::Direction;
fn create_test_pack() -> ResourcePack {
let mut pack = ResourcePack::new();
let cube_all = BlockModel {
parent: Some("block/cube".to_string()),
textures: [("particle".to_string(), "#all".to_string())]
.into_iter()
.collect(),
elements: vec![ModelElement {
from: [0.0, 0.0, 0.0],
to: [16.0, 16.0, 16.0],
rotation: None,
shade: true,
faces: Direction::ALL
.iter()
.map(|d| {
(
*d,
ModelFace {
texture: "#all".to_string(),
uv: None,
cullface: Some(*d),
rotation: 0,
tintindex: -1,
},
)
})
.collect(),
}],
..Default::default()
};
pack.add_model("minecraft", "block/cube_all", cube_all);
let cube = BlockModel {
parent: None,
ambient_occlusion: true,
textures: HashMap::new(),
elements: vec![],
..Default::default()
};
pack.add_model("minecraft", "block/cube", cube);
let stone = BlockModel {
parent: Some("block/cube_all".to_string()),
textures: [("all".to_string(), "block/stone".to_string())]
.into_iter()
.collect(),
elements: vec![],
..Default::default()
};
pack.add_model("minecraft", "block/stone", stone);
pack
}
#[test]
fn test_resolve_simple_model() {
let pack = create_test_pack();
let resolver = ModelResolver::new(&pack);
let model = resolver.resolve("minecraft:block/cube").unwrap();
assert!(model.parent.is_none());
}
#[test]
fn test_resolve_with_inheritance() {
let pack = create_test_pack();
let resolver = ModelResolver::new(&pack);
let model = resolver.resolve("minecraft:block/stone").unwrap();
assert!(!model.elements.is_empty());
assert!(model.textures.contains_key("all"));
assert_eq!(model.textures.get("all"), Some(&"block/stone".to_string()));
}
#[test]
fn test_resolve_texture_chain() {
let pack = create_test_pack();
let resolver = ModelResolver::new(&pack);
let model = resolver.resolve("minecraft:block/stone").unwrap();
let resolved_textures = resolver.resolve_textures(&model);
assert_eq!(
resolved_textures.get("particle"),
Some(&"block/stone".to_string())
);
}
#[test]
fn test_missing_model() {
let pack = create_test_pack();
let resolver = ModelResolver::new(&pack);
let result = resolver.resolve("minecraft:block/nonexistent");
assert!(result.is_err());
}
}