Skip to main content

mnk_vmf/types/entity/
entity.rs

1use chumsky::{IterParser, Parser as ChumskyParser};
2use std::collections::HashMap;
3
4use crate::{
5    impl_block_properties_parser,
6    parser::{
7        any_quoted_string, close_block, key_value, key_value_boolean, key_value_numeric,
8        open_block, quoted_string, InternalParser, TokenError, TokenSource,
9    },
10    types::{
11        entity::{parse_output_entry, EntityOutput},
12        point::{key_value_point3d, Point3D},
13        Color, EditorData, Solid,
14    },
15    Parser,
16};
17
18/// Represents a generic entity in a VMF file
19#[derive(Debug, Default)]
20pub struct Entity<'src> {
21    pub id: u32,
22    pub classname: &'src str,
23    pub origin: Option<Point3D>,
24    pub angles: Option<Point3D>,
25
26    // Common entity properties
27    pub targetname: Option<&'src str>,
28    pub parentname: Option<&'src str>,
29    pub target: Option<&'src str>,
30    pub model: Option<&'src str>,
31    pub skin: Option<u32>,
32    pub spawnflags: Option<u32>,
33    pub rendermode: Option<u32>,
34    pub renderamt: Option<u32>,
35    pub rendercolor: Option<Color>,
36    pub disableshadows: Option<bool>,
37    pub disablereceiveshadows: Option<bool>,
38    pub startdisabled: Option<bool>,
39
40    // Entity connections (outputs)
41    pub outputs: Vec<EntityOutput<'src>>,
42
43    // Custom key-value pairs for entity-specific properties
44    pub properties: HashMap<&'src str, &'src str>,
45
46    // Solids (for brush entities)
47    pub solids: Vec<Solid<'src>>,
48
49    // Editor data
50    pub editor: Option<EditorData<'src>>,
51}
52
53/// Internal [`Entity`] Properties to be used in a parser impl
54#[derive(Debug, Clone)]
55enum EntityProperty<'src> {
56    Id(u32),
57    Classname(&'src str),
58    Origin(Point3D),
59    Angles(Point3D),
60    Targetname(&'src str),
61    Parentname(&'src str),
62    Target(&'src str),
63    Model(&'src str),
64    Skin(u32),
65    SpawnFlags(u32),
66    RenderMode(u32),
67    RenderAmt(u32),
68    RenderColor(Color),
69    DisableShadows(bool),
70    DisableReceiveShadows(bool),
71    StartDisabled(bool),
72    Editor(EditorData<'src>),
73    Connections(Vec<EntityOutput<'src>>),
74    Solid(Solid<'src>),
75    Custom(&'src str, &'src str),
76}
77
78/// Parser for the connections block containing entity outputs
79fn parse_connections_block<'src, I>(
80) -> impl ChumskyParser<'src, I, Vec<EntityOutput<'src>>, TokenError<'src>>
81where
82    I: TokenSource<'src>,
83{
84    open_block("connections")
85        .ignore_then(parse_output_entry().repeated().collect())
86        .then_ignore(close_block())
87}
88
89/// Parse a color from rendercolor format "R G B"
90fn parse_rendercolor<'src, I>() -> impl ChumskyParser<'src, I, Color, TokenError<'src>>
91where
92    I: TokenSource<'src>,
93{
94    use chumsky::error::Rich;
95
96    quoted_string("rendercolor")
97        .ignore_then(any_quoted_string())
98        .try_map(|s: &str, span| {
99            let mut parts = s.split_whitespace().map(str::parse::<u8>);
100            let (r, g, b) = match (parts.next(), parts.next(), parts.next()) {
101                (Some(Ok(r)), Some(Ok(g)), Some(Ok(b))) => (r, g, b),
102                _ => return Err(Rich::custom(span, "invalid rendercolor components")),
103            };
104
105            if parts.next().is_some() {
106                return Err(Rich::custom(span, "too many rendercolor components"));
107            }
108
109            Ok(Color { r, g, b })
110        })
111}
112
113/// Public parser trait implementation that allows [`Entity`] to use ::parse(input) call.
114impl<'src> Parser<'src> for Entity<'src> {}
115
116/// A [`InternalParser`] implementation for [`Entity`].
117///
118/// usage: `let entity = Entity::parser().parse(input);`.
119///
120/// The format that is being parsed here is:
121/// ```ignore
122/// entity
123/// {
124///     "id" "7"
125///     "classname" "info_player_start"
126///     "angles" "0 90 0"
127///     "origin" "0 -256 0"
128///     editor
129///     {
130///         "color" "0 255 0"
131///         "visgroupshown" "1"
132///         "visgroupautoshown" "1"
133///     }
134/// }
135/// ```
136impl<'src> InternalParser<'src> for Entity<'src> {
137    fn parser<I>() -> impl ChumskyParser<'src, I, Self, TokenError<'src>>
138    where
139        I: TokenSource<'src>,
140    {
141        // Known property parsers
142        impl_block_properties_parser! {
143            known_properties: EntityProperty = {
144                p_id                        = key_value_numeric("id")                          => EntityProperty::Id,
145                p_classname                 = key_value("classname")                           => |s: &str| EntityProperty::Classname(s),
146                p_origin                    = key_value_point3d("origin")                      => EntityProperty::Origin,
147                p_angles                    = key_value_point3d("angles")                      => EntityProperty::Angles,
148                p_targetname                = key_value("targetname")                          => |s: &str| EntityProperty::Targetname(s),
149                p_parentname                = key_value("parentname")                          => |s: &str| EntityProperty::Parentname(s),
150                p_target                    = key_value("target")                              => |s: &str| EntityProperty::Target(s),
151                p_model                     = key_value("model")                               => |s: &str| EntityProperty::Model(s),
152                p_skin                      = key_value_numeric("skin")                        => EntityProperty::Skin,
153                p_spawnflags                = key_value_numeric("spawnflags")                  => EntityProperty::SpawnFlags,
154                p_rendermode                = key_value_numeric("rendermode")                  => EntityProperty::RenderMode,
155                p_renderamt                 = key_value_numeric("renderamt")                   => EntityProperty::RenderAmt,
156                p_rendercolor               = parse_rendercolor()                              => EntityProperty::RenderColor,
157                p_disableshadows            = key_value_boolean("disableshadows")              => EntityProperty::DisableShadows,
158                p_disablereceiveshadows     = key_value_boolean("disablereceiveshadows")       => EntityProperty::DisableReceiveShadows,
159                p_startdisabled             = key_value_boolean("startdisabled")               => EntityProperty::StartDisabled,
160            }
161        }
162
163        // Nested block parsers
164        let editor_parser = EditorData::parser().map(EntityProperty::Editor);
165        let connections_parser = parse_connections_block().map(EntityProperty::Connections);
166        let solid_parser = Solid::parser().map(EntityProperty::Solid);
167
168        // Custom property parser (catch-all for unknown properties)
169        let custom_property = any_quoted_string()
170            .then(any_quoted_string())
171            .map(|(key, value): (&str, &str)| EntityProperty::Custom(key, value));
172
173        // Combine all parsers
174        let any_property = known_properties
175            .or(editor_parser)
176            .or(connections_parser)
177            .or(solid_parser)
178            .or(custom_property);
179
180        open_block("entity")
181            .ignore_then(any_property.repeated().collect::<Vec<EntityProperty>>())
182            .then_ignore(close_block())
183            .map(|properties: Vec<EntityProperty>| {
184                let mut entity = Entity::default();
185                for prop in properties {
186                    match prop {
187                        EntityProperty::Id(val) => entity.id = val,
188                        EntityProperty::Classname(val) => entity.classname = val,
189                        EntityProperty::Origin(val) => entity.origin = Some(val),
190                        EntityProperty::Angles(val) => entity.angles = Some(val),
191                        EntityProperty::Targetname(val) => entity.targetname = Some(val),
192                        EntityProperty::Parentname(val) => entity.parentname = Some(val),
193                        EntityProperty::Target(val) => entity.target = Some(val),
194                        EntityProperty::Model(val) => entity.model = Some(val),
195                        EntityProperty::Skin(val) => entity.skin = Some(val),
196                        EntityProperty::SpawnFlags(val) => entity.spawnflags = Some(val),
197                        EntityProperty::RenderMode(val) => entity.rendermode = Some(val),
198                        EntityProperty::RenderAmt(val) => entity.renderamt = Some(val),
199                        EntityProperty::RenderColor(val) => entity.rendercolor = Some(val),
200                        EntityProperty::DisableShadows(val) => entity.disableshadows = Some(val),
201                        EntityProperty::DisableReceiveShadows(val) => {
202                            entity.disablereceiveshadows = Some(val)
203                        }
204                        EntityProperty::StartDisabled(val) => entity.startdisabled = Some(val),
205                        EntityProperty::Editor(val) => entity.editor = Some(val),
206                        EntityProperty::Connections(val) => entity.outputs = val,
207                        EntityProperty::Solid(val) => entity.solids.push(val),
208                        EntityProperty::Custom(key, value) => {
209                            entity.properties.insert(key, value);
210                        }
211                    }
212                }
213                entity
214            })
215            .boxed()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::util::lex;
223
224    #[test]
225    fn test_entity_simple_point_entity() {
226        let input = r#"
227        entity
228        {
229            "id" "7"
230            "classname" "info_player_start"
231            "angles" "0 90 0"
232            "origin" "0 -256 0"
233            editor
234            {
235                "color" "0 255 0"
236                "visgroupshown" "1"
237                "visgroupautoshown" "1"
238            }
239        }
240        "#;
241
242        let stream = lex(input);
243        let result = Entity::parse(stream);
244        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
245
246        let entity = result.unwrap();
247        assert_eq!(entity.id, 7);
248        assert_eq!(entity.classname, "info_player_start");
249        assert!(entity.angles.is_some());
250        assert_eq!(entity.angles.unwrap().y, 90.0);
251        assert!(entity.origin.is_some());
252        assert_eq!(entity.origin.unwrap().y, -256.0);
253        assert!(entity.editor.is_some());
254    }
255
256    #[test]
257    fn test_entity_with_custom_properties() {
258        let input = r#"
259        entity
260        {
261            "id" "85"
262            "classname" "light"
263            "_light" "255 255 255 400"
264            "_lightHDR" "-1 -1 -1 1"
265            "_lightscaleHDR" "1"
266            "_quadratic_attn" "1"
267            "origin" "-192 192 128"
268            editor
269            {
270                "color" "220 30 220"
271                "visgroupshown" "1"
272                "visgroupautoshown" "1"
273            }
274        }
275        "#;
276
277        let stream = lex(input);
278        let result = Entity::parse(stream);
279        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
280
281        let entity = result.unwrap();
282        assert_eq!(entity.id, 85);
283        assert_eq!(entity.classname, "light");
284        assert_eq!(entity.properties.get("_light"), Some(&"255 255 255 400"));
285        assert_eq!(entity.properties.get("_lightHDR"), Some(&"-1 -1 -1 1"));
286        assert_eq!(entity.properties.get("_lightscaleHDR"), Some(&"1"));
287        assert_eq!(entity.properties.get("_quadratic_attn"), Some(&"1"));
288    }
289
290    #[test]
291    fn test_entity_with_connections() {
292        let input = r#"
293        entity
294        {
295            "id" "243"
296            "classname" "func_button"
297            "origin" "32 -217 48"
298            connections
299            {
300                "OnIn" "motor*,TurnOn,,0,-1"
301                "OnOut" "motor*,TurnOff,,0,-1"
302            }
303        }
304        "#;
305
306        let stream = lex(input);
307        let result = Entity::parse(stream);
308        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
309
310        let entity = result.unwrap();
311        assert_eq!(entity.id, 243);
312        assert_eq!(entity.classname, "func_button");
313        assert_eq!(entity.outputs.len(), 2);
314        assert_eq!(entity.outputs[0].output_name, "OnIn");
315        assert_eq!(entity.outputs[0].target, "motor*");
316        assert_eq!(entity.outputs[0].input, "TurnOn");
317        assert_eq!(entity.outputs[1].output_name, "OnOut");
318        assert_eq!(entity.outputs[1].input, "TurnOff");
319    }
320
321    #[test]
322    fn test_entity_with_render_properties() {
323        let input = r#"
324        entity
325        {
326            "id" "100"
327            "classname" "prop_static"
328            "origin" "0 0 0"
329            "renderamt" "255"
330            "rendercolor" "255 128 64"
331            "rendermode" "0"
332            "disableshadows" "1"
333            "disablereceiveshadows" "0"
334        }
335        "#;
336
337        let stream = lex(input);
338        let result = Entity::parse(stream);
339        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
340
341        let entity = result.unwrap();
342        assert_eq!(entity.renderamt, Some(255));
343        assert!(entity.rendercolor.is_some());
344        let color = entity.rendercolor.unwrap();
345        assert_eq!(color.r, 255);
346        assert_eq!(color.g, 128);
347        assert_eq!(color.b, 64);
348        assert_eq!(entity.rendermode, Some(0));
349        assert_eq!(entity.disableshadows, Some(true));
350        assert_eq!(entity.disablereceiveshadows, Some(false));
351    }
352
353    #[test]
354    fn test_entity_with_targetname() {
355        let input = r#"
356        entity
357        {
358            "id" "50"
359            "classname" "func_door"
360            "targetname" "main_door"
361            "target" "door_trigger"
362            "parentname" "door_parent"
363            "origin" "0 0 0"
364        }
365        "#;
366
367        let stream = lex(input);
368        let result = Entity::parse(stream);
369        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
370
371        let entity = result.unwrap();
372        assert_eq!(entity.targetname, Some("main_door"));
373        assert_eq!(entity.target, Some("door_trigger"));
374        assert_eq!(entity.parentname, Some("door_parent"));
375    }
376
377    #[test]
378    fn test_entity_minimal() {
379        let input = r#"
380        entity
381        {
382            "id" "1"
383            "classname" "worldspawn"
384        }
385        "#;
386
387        let stream = lex(input);
388        let result = Entity::parse(stream);
389        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
390
391        let entity = result.unwrap();
392        assert_eq!(entity.id, 1);
393        assert_eq!(entity.classname, "worldspawn");
394        assert!(entity.origin.is_none());
395        assert!(entity.angles.is_none());
396    }
397
398    #[test]
399    fn test_entity_empty_block() {
400        let input = r#"
401        entity
402        {
403        }
404        "#;
405
406        let stream = lex(input);
407        let result = Entity::parse(stream);
408        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
409
410        let entity = result.unwrap();
411        assert_eq!(entity.id, 0);
412        assert_eq!(entity.classname, "");
413    }
414
415    #[test]
416    fn test_entity_properties_out_of_order() {
417        let input = r#"
418        entity
419        {
420            "origin" "100 200 300"
421            "classname" "test_entity"
422            "angles" "45 90 0"
423            "id" "999"
424        }
425        "#;
426
427        let stream = lex(input);
428        let result = Entity::parse(stream);
429        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
430
431        let entity = result.unwrap();
432        assert_eq!(entity.id, 999);
433        assert_eq!(entity.classname, "test_entity");
434    }
435
436    #[test]
437    fn test_entity_invalid_block_name() {
438        let input = r#"
439        wrongname
440        {
441            "id" "1"
442            "classname" "test"
443        }
444        "#;
445
446        let stream = lex(input);
447        let result = Entity::parse(stream);
448        assert!(result.is_err(), "Parser should fail on invalid block name");
449    }
450
451    #[test]
452    fn test_entity_missing_closing_brace() {
453        let input = r#"
454        entity
455        {
456            "id" "1"
457            "classname" "test"
458        "#;
459
460        let stream = lex(input);
461        let result = Entity::parse(stream);
462        assert!(
463            result.is_err(),
464            "Parser should fail on missing closing brace"
465        );
466    }
467
468    #[test]
469    fn test_entity_with_solid_brush_entity() {
470        let input = r#"
471        entity
472        {
473            "id" "243"
474            "classname" "func_button"
475            "origin" "32 -217 48"
476            solid
477            {
478                "id" "187"
479                side
480                {
481                    "id" "102"
482                    "plane" "(26 -216 54) (38 -216 54) (38 -218 54)"
483                    "material" "DEV/DEV_MEASUREGENERIC01B"
484                    "uaxis" "[1 0 0 0] 0.25"
485                    "vaxis" "[0 -1 0 0] 0.25"
486                    "rotation" "0"
487                    "lightmapscale" "16"
488                    "smoothing_groups" "0"
489                }
490                editor
491                {
492                    "color" "0 255 0"
493                    "visgroupshown" "1"
494                    "visgroupautoshown" "1"
495                }
496            }
497            editor
498            {
499                "color" "220 30 220"
500                "visgroupshown" "1"
501                "visgroupautoshown" "1"
502            }
503        }
504        "#;
505
506        let stream = lex(input);
507        let result = Entity::parse(stream);
508        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
509
510        let entity = result.unwrap();
511        assert_eq!(entity.id, 243);
512        assert_eq!(entity.classname, "func_button");
513        assert_eq!(entity.solids.len(), 1);
514        assert_eq!(entity.solids[0].id, 187);
515        assert_eq!(entity.solids[0].sides.len(), 1);
516        assert!(entity.editor.is_some());
517    }
518
519    #[test]
520    fn test_entity_with_multiple_solids() {
521        let input = r#"
522        entity
523        {
524            "id" "100"
525            "classname" "func_door"
526            "origin" "0 0 0"
527            solid
528            {
529                "id" "1"
530                side
531                {
532                    "id" "1"
533                    "plane" "(0 0 0) (1 0 0) (1 1 0)"
534                    "material" "METAL/METALDOOR001"
535                    "uaxis" "[1 0 0 0] 0.25"
536                    "vaxis" "[0 -1 0 0] 0.25"
537                }
538            }
539            solid
540            {
541                "id" "2"
542                side
543                {
544                    "id" "2"
545                    "plane" "(0 0 0) (1 0 0) (1 1 0)"
546                    "material" "METAL/METALDOOR002"
547                    "uaxis" "[1 0 0 0] 0.25"
548                    "vaxis" "[0 -1 0 0] 0.25"
549                }
550            }
551        }
552        "#;
553
554        let stream = lex(input);
555        let result = Entity::parse(stream);
556        assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
557
558        let entity = result.unwrap();
559        assert_eq!(entity.id, 100);
560        assert_eq!(entity.classname, "func_door");
561        assert_eq!(entity.solids.len(), 2);
562        assert_eq!(entity.solids[0].id, 1);
563        assert_eq!(entity.solids[1].id, 2);
564    }
565}