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#[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 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 pub outputs: Vec<EntityOutput<'src>>,
42
43 pub properties: HashMap<&'src str, &'src str>,
45
46 pub solids: Vec<Solid<'src>>,
48
49 pub editor: Option<EditorData<'src>>,
51}
52
53#[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
78fn 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
89fn 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
113impl<'src> Parser<'src> for Entity<'src> {}
115
116impl<'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 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 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 let custom_property = any_quoted_string()
170 .then(any_quoted_string())
171 .map(|(key, value): (&str, &str)| EntityProperty::Custom(key, value));
172
173 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}