Skip to main content

mnk_vmf/
vmf.rs

1use chumsky::input::Stream;
2use std::path::Path;
3
4use crate::error::VMFError;
5use crate::parser::lexer::TokenIter;
6use crate::parser::{skip_unknown_block, InternalParser};
7use crate::types::*;
8
9use chumsky::primitive::choice;
10use chumsky::IterParser;
11use chumsky::Parser as ChumskyParser;
12
13/// `VMFValue` holds types of all items from a VMF.
14#[derive(Debug)]
15pub enum VMFValue<'src> {
16    VersionInfo(VersionInfo),
17    VisGroups(Box<VisGroups<'src>>),
18    ViewSettings(Box<ViewSettings>),
19    World(Box<World<'src>>),
20    Entity(Box<Entity<'src>>),
21    Cameras(Box<Cameras<'src>>),
22    Cordon(Box<Cordon>),
23}
24
25/// VMF struct with raw file data.
26/// Use `parse()` to get parsed data that borrows from this instance.
27#[allow(clippy::upper_case_acronyms)]
28pub struct VMF {
29    data: String,
30}
31
32impl VMF {
33    /// Opens a VMF file.
34    ///
35    /// # Example
36    /// ```ignore
37    /// let vmf = VMF::open("test.vmf")?;
38    /// let data = vmf.parse()?;
39    /// // Use data..
40    /// ```
41    pub fn open(path: impl AsRef<Path>) -> Result<Self, VMFError> {
42        let data = std::fs::read_to_string(path)?;
43        Ok(VMF { data })
44    }
45
46    /// Parse the VMF file and return the parsed data.
47    /// The returned data borrows from this VMF instance.
48    pub fn parse(&self) -> Result<Vec<VMFValue>, VMFError> {
49        parse_vmf_from_str(&self.data)
50    }
51
52    /// Get the raw file content as a string slice.
53    pub fn as_str(&self) -> &str {
54        &self.data
55    }
56}
57
58/// Parse VMF data from a string slice.
59/// Uses a sequential parser that handles all top-level blocks in order.
60fn parse_vmf_from_str<'src>(src: &'src str) -> Result<Vec<VMFValue<'src>>, VMFError> {
61    let token_iter = TokenIter::new(src).map(|tok| tok.expect("valid token"));
62    let token_stream = Stream::from_iter(token_iter);
63
64    let any_block = choice((
65        VersionInfo::parser().map(VMFValue::VersionInfo),
66        VisGroups::parser().map(|v| VMFValue::VisGroups(Box::new(v))),
67        ViewSettings::parser().map(|v| VMFValue::ViewSettings(Box::new(v))),
68        World::parser().map(|v| VMFValue::World(Box::new(v))),
69        Entity::parser().map(|v| VMFValue::Entity(Box::new(v))),
70        Cameras::parser().map(|v| VMFValue::Cameras(Box::new(v))),
71        Cordon::parser().map(|v| VMFValue::Cordon(Box::new(v))),
72    ));
73
74    let any_block = any_block
75        .map(|v| Some(v))
76        .or(skip_unknown_block().map(|_| None));
77
78    let all_blocks_parser = any_block.repeated().collect::<Vec<_>>();
79
80    all_blocks_parser
81        .parse(token_stream)
82        .into_result()
83        .map(|blocks| blocks.into_iter().flatten().collect())
84        .map_err(|errors| {
85            let error_msg = errors
86                .into_iter()
87                .map(|e| format!("{:?}", e.reason()))
88                .collect::<Vec<_>>()
89                .join("; ");
90            VMFError::ParseError(format!("Failed to parse VMF: {}", error_msg))
91        })
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn full_parser_test() {
100        let vmf = VMF::open("test.vmf").expect("Failed to open VMF");
101        let data = vmf.parse().expect("Failed to parse VMF");
102
103        verify_parsed_data(&data);
104    }
105
106    fn verify_parsed_data(data: &[VMFValue]) {
107        assert!(!data.is_empty(), "VMF data should not be empty");
108        println!("Successfully parsed {} top-level blocks", data.len());
109
110        for value in data {
111            match value {
112                VMFValue::VersionInfo(v) => {
113                    println!(
114                        "VersionInfo: editor v{}, build {}",
115                        v.editor_version, v.editor_build
116                    );
117                    assert_eq!(v.editor_version, 400);
118                    assert_eq!(v.editor_build, 6157);
119                }
120                VMFValue::VisGroups(_) => println!("VisGroups parsed"),
121                VMFValue::ViewSettings(_) => println!("ViewSettings parsed"),
122                VMFValue::World(w) => {
123                    println!("World parsed with {} solids", w.solids.len());
124                    assert!(w.id == 1);
125                    assert!(w.classname == "worldspawn");
126                }
127                VMFValue::Entity(e) => println!("Entity: {:?}", e.classname),
128                VMFValue::Cameras(c) => {
129                    println!("Cameras: activecamera={}", c.activecamera);
130                    assert_eq!(c.activecamera, -1);
131                }
132                VMFValue::Cordon(_) => println!("Cordon parsed"),
133            }
134        }
135    }
136
137    #[test]
138    fn test_large_real_map() {
139        let path = Path::new("Gm_RunDownTown.vmf");
140
141        if !path.exists() {
142            eprintln!("Skipping large map test - file not found");
143            return;
144        }
145
146        println!("Parsing Gm_RunDownTown.vmf...");
147
148        let start = std::time::Instant::now();
149        let vmf = VMF::open(path).expect("Failed to open large VMF");
150        let open_time = start.elapsed();
151        println!("Open time: {:?}", open_time);
152
153        let start = std::time::Instant::now();
154        let data = vmf.parse().expect("Failed to parse large VMF");
155        let parse_time = start.elapsed();
156        println!("Parse time: {:?}", parse_time);
157
158        println!("Total blocks parsed: {}", data.len());
159
160        // Count different types
161        let mut world_count = 0;
162        let mut entity_count = 0;
163        let mut solid_count = 0;
164
165        for value in &data {
166            match value {
167                VMFValue::World(w) => {
168                    world_count += 1;
169                    solid_count += w.solids.len();
170                }
171                VMFValue::Entity(e) => {
172                    entity_count += 1;
173                    solid_count += e.solids.len();
174                }
175                _ => {}
176            }
177        }
178
179        println!("Worlds: {}", world_count);
180        println!("Entities: {}", entity_count);
181        println!("Total solids: {}", solid_count);
182        println!("Total time: {:?}", open_time + parse_time);
183    }
184}