Skip to main content

source_vmt/
vmt.rs

1use indexmap::IndexMap;
2use source_kv::{Value, Deserializer};
3use crate::interner::{VmtKey, intern_key};
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct Vmt {
7    pub shader: String,
8    pub properties: IndexMap<VmtKey, Vec<Value>>,
9}
10
11impl Vmt {
12    /// Creates a new empty VMT with the specified shader.
13    pub fn new(shader: &str) -> Self {
14        Self {
15            shader: shader.to_lowercase(),
16            properties: IndexMap::new(),
17        }
18    }
19
20    /// Parses VMT from a string using AST parser to handle root-level shader.
21    pub fn from_str(input: &str) -> Result<Self, source_kv::Error> {
22        let mut de = Deserializer::from_str(input);
23        let root = de.parse_root()?;
24
25        if let Value::Obj(mut root_map) = root {
26            if let Some((shader, mut values)) = root_map.pop() {
27                if let Some(Value::Obj(props)) = values.pop() {
28                    let mut properties = IndexMap::with_capacity(props.len());
29                    for (k, v) in props {
30                        properties.insert(intern_key(&k), v);
31                    }
32                    return Ok(Self {
33                        shader: shader.to_lowercase(),
34                        properties
35                    });
36                }
37            }
38        }
39        Err(source_kv::Error::Message("Invalid VMT: Missing shader root or body".into()))
40    }
41
42    /// Parses VMT from a file.
43    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, source_kv::Error> {
44        let content = std::fs::read_to_string(path)?;
45        Self::from_str(&content)
46    }
47
48    /// Set a string property. Overwrites if exists.
49    pub fn set_string(&mut self, key: &str, value: &str) -> &mut Self {
50        self.properties.insert(
51            intern_key(key),
52            vec![Value::Str(value.to_string())]
53        );
54        self
55    }
56
57    /// Set a flag (boolean) property (converts to "1" or "0").
58    pub fn set_flag(&mut self, key: &str, enabled: bool) -> &mut Self {
59        self.set_string(key, if enabled { "1" } else { "0" })
60    }
61
62    /// Removes a property by key, checking for $, % and raw name.
63    pub fn remove(&mut self, key: &str) -> &mut Self {
64        let base = key.to_lowercase();
65        self.properties.shift_remove(base.as_str());
66        self.properties.shift_remove(format!("${}", base).as_str());
67        self.properties.shift_remove(format!("%{}", base).as_str());
68        self
69    }
70
71    /// O(1) lookup. Handles $, % prefixes and case-insensitive keys.
72    pub fn get_raw(&self, key: &str) -> Option<&Value> {
73        let base = key.to_lowercase();
74        self.properties.get(base.as_str())
75            .or_else(|| self.properties.get(format!("${}", base).as_str()))
76            .or_else(|| self.properties.get(format!("%{}", base).as_str()))
77            .or_else(|| {
78                if base.starts_with('$') || base.starts_with('%') {
79                    let raw = &base[1..];
80                    self.properties.get(raw)
81                        .or_else(|| self.properties.get(format!("${}", raw).as_str()))
82                        .or_else(|| self.properties.get(format!("%{}", raw).as_str()))
83                } else {
84                    None
85                }
86            })
87            .and_then(|v| v.first())
88    }
89
90    /// Get value as string if it exists.
91    pub fn get_string(&self, key: &str) -> Option<String> {
92        self.get_raw(key).and_then(|v| v.as_str().map(String::from))
93    }
94
95    /// Safely gets a value and parses it as f32.
96    pub fn get_f32(&self, key: &str) -> Option<f32> {
97        self.get_string(key)?.parse::<f32>().ok()
98    }
99
100    /// Safely gets a value and parses it as i32.
101    pub fn get_i32(&self, key: &str) -> Option<i32> {
102        self.get_string(key)?.parse::<i32>().ok()
103    }
104
105    /// Checks boolean flags: supports "1", "true", "yes".
106    pub fn get_bool(&self, key: &str) -> bool {
107        match self.get_string(key).as_deref() {
108            Some("1") | Some("true") => true,
109            _ => false,
110        }
111    }
112
113    /// Parses colors/vectors in both [0.0 0.0 0.0] and {255 255 255} formats.
114    /// Returns a normalized [f32; 3] (0.0 to 1.0).
115    pub fn get_color(&self, key: &str) -> Option<[f32; 3]> {
116        let val = self.get_string(key)?;
117        let val = val.trim();
118
119        if val.starts_with('[') && val.ends_with(']') {
120            let parts: Vec<f32> = val[1..val.len()-1]
121                .split_whitespace()
122                .filter_map(|s| s.parse().ok())
123                .collect();
124            if parts.len() >= 3 { return Some([parts[0], parts[1], parts[2]]); }
125        } else if val.starts_with('{') && val.ends_with('}') {
126            let parts: Vec<f32> = val[1..val.len()-1]
127                .split_whitespace()
128                .filter_map(|s| s.parse::<u8>().ok().map(|v| v as f32 / 255.0))
129                .collect();
130            if parts.len() >= 3 { return Some([parts[0], parts[1], parts[2]]); }
131        }
132        None
133    }
134
135    /// Adds a proxy to the material.
136    pub fn add_proxy<'a, I>(&mut self, name: &str, params: I) -> &mut Self
137    where
138        I: IntoIterator<Item = (&'a str, &'a str)>,
139    {
140        let name_lower = name.to_lowercase();
141        let mut proxy_params = IndexMap::new();
142        for (k, v) in params {
143            proxy_params.insert(k.to_string(), vec![Value::Str(v.to_string())]);
144        }
145
146        let proxy_obj = Value::Obj(proxy_params);
147
148        let proxies_vec = self.properties.entry(intern_key("proxies"))
149            .or_insert_with(|| vec![Value::Obj(IndexMap::new())]);
150
151        if let Some(Value::Obj(map)) = proxies_vec.first_mut() {
152            map.entry(name_lower)
153               .or_insert_with(Vec::new)
154               .push(proxy_obj);
155        }
156
157        self
158    }
159
160    /// Serializes the VMT back into a KeyValues string.
161    pub fn to_string(&self) -> Result<String, source_kv::Error> {
162        let mut root_map = IndexMap::new();
163        let mut props = IndexMap::new();
164
165        for (k, v) in &self.properties {
166            props.insert(k.to_string(), v.clone());
167        }
168
169        root_map.insert(self.shader.clone(), vec![Value::Obj(props)]);
170
171        source_kv::to_string(&Value::Obj(root_map))
172    }
173
174    /// Serializes the VMT and writes it to a file.
175    pub fn to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), source_kv::Error> {
176        let content = self.to_string()?;
177        std::fs::write(path, content)?;
178        Ok(())
179    }
180}