use std::collections::HashMap;
use std::sync::Arc;
use std::path::PathBuf;
use crate::vmt::Vmt;
use source_fs::{FileSystem, traits::PackFile, providers::DummyVpk, FileSystemError};
pub struct MaterialSystem<P: PackFile = DummyVpk> {
pub fs: FileSystem<P>,
cache: HashMap<String, Arc<Vmt>>,
resolved_cache: HashMap<String, Arc<Vmt>>,
paths: HashMap<String, PathBuf>,
fallback: Option<Arc<Vmt>>,
pub search_path: String,
pub prioritize_vpks: bool,
}
impl<P: PackFile> MaterialSystem<P> {
pub fn new(fs: FileSystem<P>) -> Self {
Self {
fs,
cache: HashMap::new(),
resolved_cache: HashMap::new(),
paths: HashMap::new(),
fallback: None,
search_path: "game".to_string(),
prioritize_vpks: false,
}
}
pub fn from_path(path: impl AsRef<std::path::Path>) -> Result<MaterialSystem<DummyVpk>, FileSystemError> {
let fs = source_fs::create_fs(path)?;
Ok(MaterialSystem::<DummyVpk>::new(fs))
}
pub fn set_fallback(&mut self, vmt: Vmt) {
self.fallback = Some(Arc::new(vmt));
}
pub fn register(&mut self, path: &str, vmt: Vmt) {
self.cache.insert(path.to_lowercase(), Arc::new(vmt));
}
pub fn with_search_path(mut self, path: &str) -> Self {
self.search_path = path.to_string();
self
}
pub fn prioritize_vpks(mut self, prioritize: bool) -> Self {
self.prioritize_vpks = prioritize;
self
}
pub fn get_material(&mut self, path: &str) -> Result<Arc<Vmt>, crate::Error> {
let path_lower = path.to_lowercase();
if let Some(cached) = self.cache.get(&path_lower) {
return Ok(Arc::clone(cached));
}
if let Some(file_path) = self.fs.find_asset(&path_lower, "materials/", ".vmt", &self.search_path) {
self.paths.insert(path_lower.clone(), file_path);
}
if let Some(data) = self.fs.read_material_str(&path_lower, &self.search_path, self.prioritize_vpks) {
let vmt = Vmt::from_str(&data)?;
let arc_vmt = Arc::new(vmt);
self.cache.insert(path_lower, Arc::clone(&arc_vmt));
return Ok(arc_vmt);
}
if let Some(fallback) = &self.fallback {
Ok(Arc::clone(fallback))
} else {
Err(crate::Error::Message(format!("Material not found: {}", path)))
}
}
pub fn get_resolved_material(&mut self, path: &str) -> Result<Arc<Vmt>, crate::Error> {
let path_lower = path.to_lowercase();
if let Some(cached) = self.resolved_cache.get(&path_lower) {
return Ok(Arc::clone(cached));
}
let vmt = self.get_material(&path_lower)?;
if vmt.shader == "patch" {
let resolved = self.resolve_patch(&vmt)?;
let arc_resolved = Arc::new(resolved);
self.resolved_cache.insert(path_lower, Arc::clone(&arc_resolved));
return Ok(arc_resolved);
}
Ok(vmt)
}
pub fn get_material_mut(&mut self, path: &str) -> Result<&mut Vmt, crate::Error> {
let path_lower = path.to_lowercase();
self.get_material(&path_lower)?;
self.resolved_cache.remove(&path_lower);
let arc = self.cache.get_mut(&path_lower)
.ok_or_else(|| crate::Error::Message("Material missing from cache".into()))?;
Ok(Arc::make_mut(arc))
}
pub fn resolve_patch(&mut self, patch: &Vmt) -> Result<Vmt, crate::Error> {
let include_path = patch.get_string("include")
.or_else(|| patch.get_string("$include"))
.ok_or_else(|| crate::Error::Message("Patch material missing 'include' key".into()))?;
let base_arc = self.get_resolved_material(&include_path)?;
let mut base_owned = (*base_arc).clone();
base_owned.apply_patch(patch);
Ok(base_owned)
}
pub fn get_material_path(&self, path: &str) -> Option<&PathBuf> {
self.paths.get(&path.to_lowercase())
}
}