Skip to main content

mnk_vmf/types/
side.rs

1use chumsky::IterParser;
2use chumsky::Parser as ChumskyParser;
3
4use crate::impl_block_properties_parser;
5use crate::parser::{
6    close_block, key_value, key_value_numeric, open_block, skip_unknown_block, InternalParser,
7    TokenError, TokenSource,
8};
9use crate::types::point::key_value_plane;
10use crate::types::textureaxis::key_value_texture_axis;
11use crate::Parser;
12
13use super::point::Point3D;
14use super::textureaxis::TextureAxis;
15use super::DispInfo;
16
17/// Represents a side (face) of a solid brush
18#[derive(Debug, Default, Clone)]
19pub struct Side<'src> {
20    pub id: u32,
21    pub plane: (Point3D, Point3D, Point3D),
22    pub material: &'src str,
23    pub uaxis: TextureAxis,
24    pub vaxis: TextureAxis,
25    pub rotation: f32,
26    pub lightmapscale: u32,
27    pub smoothing_groups: u32,
28    pub dispinfo: Option<DispInfo>, // Displacement information for terrain
29}
30
31/// Side properties used for parser impl
32enum SideProperty<'src> {
33    Id(u32),
34    Plane((Point3D, Point3D, Point3D)),
35    Material(&'src str),
36    UAxis(TextureAxis),
37    VAxis(TextureAxis),
38    Rotation(f32),
39    LightmapScale(u32),
40    SmoothingGroups(u32),
41    DispInfo(DispInfo),
42}
43
44/// Public parser trait implementation that allows [`Side`] to use ::parse(input) call.
45impl<'src> Parser<'src> for Side<'src> {}
46
47/// A [`Side`] implementation for [`Side`].
48/// Every key-value pair needs to be in order, like in the example bellow.
49///
50/// usage: `let side = Side::parser().parse(input);`.
51///
52/// The format that is being parsed here is:
53/// side
54/// {
55///     "id" "1"
56///     "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
57///     "material" "DEV/DEV_MEASUREGENERIC01B"
58///     "uaxis" "[1 0 0 0] 0.25"
59///     "vaxis" "[0 -1 0 0] 0.25"
60///     "rotation" "0"
61///     "lightmapscale" "16"
62///     "smoothing_groups" "0"
63/// }
64impl<'src> InternalParser<'src> for Side<'src> {
65    fn parser<I>() -> impl ChumskyParser<'src, I, Self, TokenError<'src>>
66    where
67        I: TokenSource<'src>,
68    {
69        impl_block_properties_parser! {
70            property_list: SideProperty = {
71                p_id                  = key_value_numeric("id")                 => SideProperty::Id,
72                p_plane               = key_value_plane("plane")                => SideProperty::Plane,
73                p_material            = key_value("material")                   => SideProperty::Material,
74                p_uaxis               = key_value_texture_axis("uaxis")         => SideProperty::UAxis,
75                p_vaxis               = key_value_texture_axis("vaxis")         => SideProperty::VAxis,
76                p_rotation            = key_value_numeric("rotation")           => SideProperty::Rotation,
77                p_lightmap_scale      = key_value_numeric("lightmapscale")      => SideProperty::LightmapScale,
78                p_smoothing_groups    = key_value_numeric("smoothing_groups")   => SideProperty::SmoothingGroups,
79            }
80        }
81
82        let dispinfo_parser = DispInfo::parser().map(SideProperty::DispInfo);
83        let any_property_or_block = property_list
84            .or(dispinfo_parser)
85            .map(Some)
86            .or(skip_unknown_block().map(|_| None));
87
88        open_block("side")
89            .ignore_then(
90                any_property_or_block
91                    .repeated()
92                    .collect::<Vec<Option<SideProperty>>>(),
93            )
94            .then_ignore(close_block())
95            .map(|properties: Vec<Option<SideProperty>>| {
96                let mut side = Side::default();
97                for prop_opt in properties {
98                    if let Some(prop) = prop_opt {
99                        match prop {
100                            SideProperty::Id(val) => side.id = val,
101                            SideProperty::Plane(val) => side.plane = val,
102                            SideProperty::Material(val) => side.material = val,
103                            SideProperty::UAxis(val) => side.uaxis = val,
104                            SideProperty::VAxis(val) => side.vaxis = val,
105                            SideProperty::Rotation(val) => side.rotation = val,
106                            SideProperty::LightmapScale(val) => side.lightmapscale = val,
107                            SideProperty::SmoothingGroups(val) => side.smoothing_groups = val,
108                            SideProperty::DispInfo(val) => side.dispinfo = Some(val),
109                        }
110                    }
111                }
112                side
113            })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::util::lex;
120
121    use super::*;
122    use chumsky::Parser as ChumskyParser;
123
124    #[test]
125    fn test_parse_side_complete_valid_order() {
126        let input = r#"
127        side
128        {
129            "id" "1"
130            "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
131            "material" "DEV/DEV_MEASUREGENERIC01B"
132            "uaxis" "[1 0 0 0] 0.25"
133            "vaxis" "[0 -1 0 0] 0.25"
134            "rotation" "0"
135            "lightmapscale" "16"
136            "smoothing_groups" "0"
137        }
138        "#;
139        let stream = lex(input);
140        let result = Side::parser().parse(stream).into_result();
141
142        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
143        let side = result.unwrap();
144
145        let expected_plane = (
146            Point3D {
147                x: -320.0,
148                y: -320.0,
149                z: 0.0,
150            },
151            Point3D {
152                x: -320.0,
153                y: 320.0,
154                z: 0.0,
155            },
156            Point3D {
157                x: 320.0,
158                y: 320.0,
159                z: 0.0,
160            },
161        );
162        let expected_uaxis = TextureAxis {
163            x: 1.0,
164            y: 0.0,
165            z: 0.0,
166            shift: 0.0,
167            scale: 0.25,
168        };
169        let expected_vaxis = TextureAxis {
170            x: 0.0,
171            y: -1.0,
172            z: 0.0,
173            shift: 0.0,
174            scale: 0.25,
175        };
176
177        assert_eq!(side.id, 1);
178        assert_eq!(side.plane, expected_plane);
179        assert_eq!(side.material, "DEV/DEV_MEASUREGENERIC01B");
180        assert_eq!(side.uaxis, expected_uaxis);
181        assert_eq!(side.vaxis, expected_vaxis);
182        assert_eq!(side.rotation, 0.0);
183        assert_eq!(side.lightmapscale, 16);
184        assert_eq!(side.smoothing_groups, 0);
185    }
186
187    #[test]
188    fn test_parse_side_properties_out_of_order() {
189        let input = r#"
190        side
191        {
192            "material" "BRICK/BRICKWALL001A"
193            "id" "42"
194            "uaxis" "[0 1 0 10] 0.125"
195            "smoothing_groups" "1"
196            "plane" "(0 0 0) (100 0 0) (100 100 0)"
197            "lightmapscale" "32"
198            "vaxis" "[1 0 0 20] 0.125"
199            "rotation" "90"
200        }
201        "#;
202        let stream = lex(input);
203        let result = Side::parser().parse(stream).into_result();
204
205        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
206        let side = result.unwrap();
207
208        let expected_plane = (
209            Point3D {
210                x: 0.0,
211                y: 0.0,
212                z: 0.0,
213            },
214            Point3D {
215                x: 100.0,
216                y: 0.0,
217                z: 0.0,
218            },
219            Point3D {
220                x: 100.0,
221                y: 100.0,
222                z: 0.0,
223            },
224        );
225        let expected_uaxis = TextureAxis {
226            x: 0.0,
227            y: 1.0,
228            z: 0.0,
229            shift: 10.0,
230            scale: 0.125,
231        };
232        let expected_vaxis = TextureAxis {
233            x: 1.0,
234            y: 0.0,
235            z: 0.0,
236            shift: 20.0,
237            scale: 0.125,
238        };
239
240        assert_eq!(side.id, 42);
241        assert_eq!(side.plane, expected_plane);
242        assert_eq!(side.material, "BRICK/BRICKWALL001A");
243        assert_eq!(side.uaxis, expected_uaxis);
244        assert_eq!(side.vaxis, expected_vaxis);
245        assert_eq!(side.rotation, 90.0);
246        assert_eq!(side.lightmapscale, 32);
247        assert_eq!(side.smoothing_groups, 1);
248    }
249
250    #[test]
251    fn test_parse_side_missing_optional_properties() {
252        let input = r#"
253        side
254        {
255            "id" "3"
256            "plane" "(1 1 1) (2 2 2) (3 3 3)"
257            "material" "CONCRETE/CONCRETEFLOOR001"
258            "uaxis" "[1 0 0 0] 1"
259            "vaxis" "[0 1 0 0] 1"
260        }
261        "#;
262        let stream = lex(input);
263        let result = Side::parser().parse(stream).into_result();
264
265        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
266        let side = result.unwrap();
267        let default_side_for_missing_fields = Side::default(); // To get default values
268
269        let expected_plane = (
270            Point3D {
271                x: 1.0,
272                y: 1.0,
273                z: 1.0,
274            },
275            Point3D {
276                x: 2.0,
277                y: 2.0,
278                z: 2.0,
279            },
280            Point3D {
281                x: 3.0,
282                y: 3.0,
283                z: 3.0,
284            },
285        );
286        let expected_uaxis = TextureAxis {
287            x: 1.0,
288            y: 0.0,
289            z: 0.0,
290            shift: 0.0,
291            scale: 1.0,
292        };
293        let expected_vaxis = TextureAxis {
294            x: 0.0,
295            y: 1.0,
296            z: 0.0,
297            shift: 0.0,
298            scale: 1.0,
299        };
300
301        assert_eq!(side.id, 3);
302        assert_eq!(side.plane, expected_plane);
303        assert_eq!(side.material, "CONCRETE/CONCRETEFLOOR001");
304        assert_eq!(side.uaxis, expected_uaxis);
305        assert_eq!(side.vaxis, expected_vaxis);
306        assert_eq!(side.rotation, default_side_for_missing_fields.rotation);
307        assert_eq!(
308            side.lightmapscale,
309            default_side_for_missing_fields.lightmapscale
310        );
311        assert_eq!(
312            side.smoothing_groups,
313            default_side_for_missing_fields.smoothing_groups
314        );
315    }
316
317    #[test]
318    fn test_parse_side_empty_block() {
319        let input = r#"
320        side
321        {
322        }
323        "#;
324        let stream = lex(input);
325        let result = Side::parser().parse(stream).into_result();
326
327        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
328        let side = result.unwrap();
329        let expected_side = Side::default(); // Assumes all fields get their default values
330
331        assert_eq!(side.id, expected_side.id);
332        assert_eq!(side.plane, expected_side.plane);
333        assert_eq!(side.material, expected_side.material);
334        assert_eq!(side.uaxis, expected_side.uaxis);
335        assert_eq!(side.vaxis, expected_side.vaxis);
336        assert_eq!(side.rotation, expected_side.rotation);
337        assert_eq!(side.lightmapscale, expected_side.lightmapscale);
338        assert_eq!(side.smoothing_groups, expected_side.smoothing_groups);
339    }
340
341    #[test]
342    fn test_parse_side_malformed_id() {
343        let input = r#"
344        side
345        {
346            "id" "not_a_number"
347            "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
348            "material" "DEV/DEV_MEASUREGENERIC01B"
349            "uaxis" "[1 0 0 0] 0.25"
350            "vaxis" "[0 -1 0 0] 0.25"
351            "rotation" "0"
352            "lightmapscale" "16"
353            "smoothing_groups" "0"
354        }
355        "#;
356        let stream = lex(input);
357        let result = Side::parser().parse(stream).into_result();
358        assert!(
359            result.is_err(),
360            "Parsing should have failed for malformed id"
361        );
362    }
363
364    #[test]
365    fn test_parse_side_malformed_plane() {
366        let input = r#"
367        side
368        {
369            "id" "1"
370            "plane" "this_is_not_a_plane"
371            "material" "DEV/DEV_MEASUREGENERIC01B"
372            "uaxis" "[1 0 0 0] 0.25"
373            "vaxis" "[0 -1 0 0] 0.25"
374            "rotation" "0"
375            "lightmapscale" "16"
376            "smoothing_groups" "0"
377        }
378        "#;
379        let stream = lex(input);
380        let result = Side::parser().parse(stream).into_result();
381        assert!(
382            result.is_err(),
383            "Parsing should have failed for malformed plane"
384        );
385    }
386
387    #[test]
388    fn test_parse_side_malformed_uaxis() {
389        let input = r#"
390        side
391        {
392            "id" "1"
393            "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
394            "material" "DEV/DEV_MEASUREGENERIC01B"
395            "uaxis" "not_a_uaxis"
396            "vaxis" "[0 -1 0 0] 0.25"
397            "rotation" "0"
398            "lightmapscale" "16"
399            "smoothing_groups" "0"
400        }
401        "#;
402        let stream = lex(input);
403        let result = Side::parser().parse(stream).into_result();
404        assert!(
405            result.is_err(),
406            "Parsing should have failed for malformed uaxis"
407        );
408    }
409
410    #[test]
411    fn test_parse_side_missing_closing_brace_for_block() {
412        let input = r#"
413        side
414        {
415            "id" "1"
416            "plane" "(-320 -320 0) (-320 320 0) (320 320 0)"
417            "material" "DEV/DEV_MEASUREGENERIC01B"
418            "uaxis" "[1 0 0 0] 0.25"
419            "vaxis" "[0 -1 0 0] 0.25"
420            "rotation" "0"
421            "lightmapscale" "16"
422            "smoothing_groups" "0"
423        "#;
424        let stream = lex(input);
425        let result = Side::parser().parse(stream).into_result();
426        assert!(
427            result.is_err(),
428            "Parsing should have failed for missing closing brace"
429        );
430    }
431
432    #[test]
433    fn test_parse_side_unknown_property() {
434        let input = r#"
435        side
436        {
437            "id" "1"
438            "unknown_property" "some_value"
439            "material" "DEV/DEV_MEASUREGENERIC01B"
440        }
441        "#;
442        let stream = lex(input);
443        let result = Side::parser().parse(stream).into_result();
444
445        assert!(
446            result.is_err(),
447            "Parsing should fail on unknown property if not explicitly skipped"
448        );
449    }
450}