Skip to main content

mnk_vmf/types/
solid.rs

1use chumsky::{IterParser, Parser as ChumskyParser};
2
3use crate::{
4    impl_block_properties_parser,
5    parser::{close_block, key_value_numeric, open_block, InternalParser, TokenError, TokenSource},
6    types::{EditorData, Side},
7    Parser,
8};
9
10/// Represents a solid brush in the VMF file
11#[derive(Debug, Default, Clone)]
12pub struct Solid<'src> {
13    pub id: u32,
14    pub sides: Vec<Side<'src>>,
15    pub editor: Option<EditorData<'src>>,
16}
17
18/// Internal [`Solid`] Properties to be used in a parser impl
19#[derive(Debug, Clone)]
20enum SolidProperty<'src> {
21    Id(u32),
22    Side(Side<'src>),
23    Editor(EditorData<'src>),
24}
25
26/// Public parser trait implementation that allows [`Solid`] to use ::parse(input) call.
27impl<'src> Parser<'src> for Solid<'src> {}
28
29/// A [`InternalParser`] implementation for [`Solid`].
30///
31/// usage: `let solid = Solid::parser().parse(input);`.
32///
33/// The format that is being parsed here is:
34/// ```ignore
35/// solid
36/// {
37///     "id" "9"
38///     side
39///     {
40///         "id" "1"
41///         "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
42///         "material" "DEV/DEV_MEASUREGENERIC01B"
43///         "uaxis" "[1 0 0 0] 0.25"
44///         "vaxis" "[0 -1 0 0] 0.25"
45///         "rotation" "0"
46///         "lightmapscale" "16"
47///         "smoothing_groups" "0"
48///     }
49///     side
50///     {
51///         ...
52///     }
53///     editor
54///     {
55///         "color" "0 111 152"
56///         "visgroupshown" "1"
57///         "visgroupautoshown" "1"
58///     }
59/// }
60/// ```
61impl<'src> InternalParser<'src> for Solid<'src> {
62    fn parser<I>() -> impl ChumskyParser<'src, I, Self, TokenError<'src>>
63    where
64        I: TokenSource<'src>,
65    {
66        impl_block_properties_parser! {
67            property_list: SolidProperty = {
68                p_id = key_value_numeric("id") => SolidProperty::Id,
69            }
70        }
71
72        // Nested block parsers
73        let side_parser = Side::parser().map(SolidProperty::Side);
74        let editor_parser = EditorData::parser().map(SolidProperty::Editor);
75
76        // Combine all parsers
77        let any_property = property_list.or(side_parser).or(editor_parser);
78
79        open_block("solid")
80            .ignore_then(any_property.repeated().collect::<Vec<SolidProperty>>())
81            .then_ignore(close_block())
82            .map(|properties: Vec<SolidProperty>| {
83                let mut solid = Solid::default();
84                for prop in properties {
85                    match prop {
86                        SolidProperty::Id(val) => solid.id = val,
87                        SolidProperty::Side(val) => solid.sides.push(val),
88                        SolidProperty::Editor(val) => solid.editor = Some(val),
89                    }
90                }
91                solid
92            })
93            .boxed()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::util::lex;
101
102    #[test]
103    fn test_solid_complete() {
104        let input = r#"
105        solid
106        {
107            "id" "9"
108            side
109            {
110                "id" "1"
111                "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
112                "material" "DEV/DEV_MEASUREGENERIC01B"
113                "uaxis" "[1 0 0 0] 0.25"
114                "vaxis" "[0 -1 0 0] 0.25"
115                "rotation" "0"
116                "lightmapscale" "16"
117                "smoothing_groups" "0"
118            }
119            side
120            {
121                "id" "2"
122                "plane" "(-320 320 -64) (-320 -320 -64) (320 -320 -64)"
123                "material" "DEV/DEV_MEASUREGENERIC01B"
124                "uaxis" "[1 0 0 0] 0.25"
125                "vaxis" "[0 -1 0 0] 0.25"
126                "rotation" "0"
127                "lightmapscale" "16"
128                "smoothing_groups" "0"
129            }
130            editor
131            {
132                "color" "0 111 152"
133                "visgroupshown" "1"
134                "visgroupautoshown" "1"
135            }
136        }
137        "#;
138
139        let stream = lex(input);
140        let result = Solid::parse(stream);
141        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
142
143        let solid = result.unwrap();
144        assert_eq!(solid.id, 9);
145        assert_eq!(solid.sides.len(), 2);
146        assert!(solid.editor.is_some());
147        assert_eq!(solid.sides[0].id, 1);
148        assert_eq!(solid.sides[1].id, 2);
149    }
150
151    #[test]
152    fn test_solid_minimal() {
153        let input = r#"
154        solid
155        {
156            "id" "100"
157            side
158            {
159                "id" "1"
160                "plane" "(0 0 0) (1 0 0) (1 1 0)"
161                "material" "DEV/DEV_MEASUREGENERIC01B"
162                "uaxis" "[1 0 0 0] 0.25"
163                "vaxis" "[0 -1 0 0] 0.25"
164            }
165        }
166        "#;
167
168        let stream = lex(input);
169        let result = Solid::parse(stream);
170        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
171
172        let solid = result.unwrap();
173        assert_eq!(solid.id, 100);
174        assert_eq!(solid.sides.len(), 1);
175        assert!(solid.editor.is_none());
176    }
177
178    #[test]
179    fn test_solid_multiple_sides() {
180        let input = r#"
181        solid
182        {
183            "id" "50"
184            side
185            {
186                "id" "1"
187                "plane" "(0 0 0) (1 0 0) (1 1 0)"
188                "material" "BRICK/BRICKWALL001A"
189                "uaxis" "[1 0 0 0] 0.25"
190                "vaxis" "[0 -1 0 0] 0.25"
191            }
192            side
193            {
194                "id" "2"
195                "plane" "(0 0 0) (1 0 0) (1 1 0)"
196                "material" "CONCRETE/CONCRETEWALL001"
197                "uaxis" "[1 0 0 0] 0.25"
198                "vaxis" "[0 -1 0 0] 0.25"
199            }
200            side
201            {
202                "id" "3"
203                "plane" "(0 0 0) (1 0 0) (1 1 0)"
204                "material" "METAL/METALWALL001"
205                "uaxis" "[1 0 0 0] 0.25"
206                "vaxis" "[0 -1 0 0] 0.25"
207            }
208            editor
209            {
210                "color" "255 128 64"
211                "visgroupshown" "1"
212                "visgroupautoshown" "1"
213            }
214        }
215        "#;
216
217        let stream = lex(input);
218        let result = Solid::parse(stream);
219        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
220
221        let solid = result.unwrap();
222        assert_eq!(solid.id, 50);
223        assert_eq!(solid.sides.len(), 3);
224        assert_eq!(solid.sides[0].material, "BRICK/BRICKWALL001A");
225        assert_eq!(solid.sides[1].material, "CONCRETE/CONCRETEWALL001");
226        assert_eq!(solid.sides[2].material, "METAL/METALWALL001");
227    }
228
229    #[test]
230    fn test_solid_out_of_order() {
231        let input = r#"
232        solid
233        {
234            side
235            {
236                "id" "1"
237                "plane" "(0 0 0) (1 0 0) (1 1 0)"
238                "material" "DEV/DEV_MEASUREGENERIC01B"
239                "uaxis" "[1 0 0 0] 0.25"
240                "vaxis" "[0 -1 0 0] 0.25"
241            }
242            "id" "75"
243            editor
244            {
245                "color" "0 255 0"
246                "visgroupshown" "1"
247                "visgroupautoshown" "1"
248            }
249        }
250        "#;
251
252        let stream = lex(input);
253        let result = Solid::parse(stream);
254        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
255
256        let solid = result.unwrap();
257        assert_eq!(solid.id, 75);
258        assert_eq!(solid.sides.len(), 1);
259        assert!(solid.editor.is_some());
260    }
261
262    #[test]
263    fn test_solid_empty_block() {
264        let input = r#"
265        solid
266        {
267        }
268        "#;
269
270        let stream = lex(input);
271        let result = Solid::parse(stream);
272        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
273
274        let solid = result.unwrap();
275        assert_eq!(solid.id, 0);
276        assert_eq!(solid.sides.len(), 0);
277    }
278
279    #[test]
280    fn test_solid_no_editor() {
281        let input = r#"
282        solid
283        {
284            "id" "200"
285            side
286            {
287                "id" "1"
288                "plane" "(0 0 0) (1 0 0) (1 1 0)"
289                "material" "DEV/DEV_MEASUREGENERIC01B"
290                "uaxis" "[1 0 0 0] 0.25"
291                "vaxis" "[0 -1 0 0] 0.25"
292            }
293        }
294        "#;
295
296        let stream = lex(input);
297        let result = Solid::parse(stream);
298        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
299
300        let solid = result.unwrap();
301        assert_eq!(solid.id, 200);
302        assert_eq!(solid.sides.len(), 1);
303        assert!(solid.editor.is_none());
304    }
305
306    #[test]
307    fn test_solid_invalid_block_name() {
308        let input = r#"
309        wrongname
310        {
311            "id" "1"
312        }
313        "#;
314
315        let stream = lex(input);
316        let result = Solid::parse(stream);
317        assert!(result.is_err(), "Parser should fail on invalid block name");
318    }
319
320    #[test]
321    fn test_solid_missing_closing_brace() {
322        let input = r#"
323        solid
324        {
325            "id" "1"
326            side
327            {
328                "id" "1"
329                "plane" "(0 0 0) (1 0 0) (1 1 0)"
330                "material" "DEV/DEV_MEASUREGENERIC01B"
331                "uaxis" "[1 0 0 0] 0.25"
332                "vaxis" "[0 -1 0 0] 0.25"
333            }
334        "#;
335
336        let stream = lex(input);
337        let result = Solid::parse(stream);
338        assert!(
339            result.is_err(),
340            "Parser should fail on missing closing brace"
341        );
342    }
343}