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, InternalParser, TokenError, TokenSource,
9 },
10 types::{EditorData, Solid},
11 Parser,
12};
13
14use super::Group;
15
16#[derive(Debug, Default)]
18pub struct World<'src> {
19 pub id: u32,
20 pub mapversion: u32,
21 pub classname: &'src str,
22 pub detailmaterial: Option<&'src str>,
23 pub detailvbsp: Option<&'src str>,
24 pub maxpropscreenwidth: Option<i32>,
25 pub skyname: Option<&'src str>,
26 pub sounds: Option<u32>,
27 pub maxrange: Option<f32>,
28
29 pub maxoccludeearea: Option<f32>,
31 pub minoccluderarea: Option<f32>,
32 pub maxoccludeearea_csgo: Option<f32>,
33 pub minoccluderarea_csgo: Option<f32>,
34 pub difficulty_level: Option<u32>,
35 pub hdr_level: Option<u32>,
36
37 pub solids: Vec<Solid<'src>>,
39
40 pub targetname: Option<&'src str>,
42 pub target: Option<&'src str>,
43
44 pub properties: HashMap<&'src str, &'src str>,
46
47 pub hidden: Option<bool>,
49 pub group: Option<Group<'src>>,
50 pub editor: Option<EditorData<'src>>,
51}
52
53#[derive(Debug, Clone)]
55enum WorldProperty<'src> {
56 Id(u32),
57 MapVersion(u32),
58 Classname(&'src str),
59 DetailMaterial(&'src str),
60 DetailVbsp(&'src str),
61 MaxPropScreenWidth(i32),
62 Skyname(&'src str),
63 Sounds(u32),
64 MaxRange(f32),
65 MaxOccludeeArea(f32),
66 MinOccluderArea(f32),
67 MaxOccludeeAreaCsgo(f32),
68 MinOccluderAreaCsgo(f32),
69 DifficultyLevel(u32),
70 HdrLevel(u32),
71 Targetname(&'src str),
72 Target(&'src str),
73 Hidden(bool),
74 Group(Group<'src>),
75 Editor(EditorData<'src>),
76 Solid(Solid<'src>),
77 Custom(&'src str, &'src str),
78}
79
80impl<'src> Parser<'src> for World<'src> {}
82
83impl<'src> InternalParser<'src> for World<'src> {
85 fn parser<I>() -> impl ChumskyParser<'src, I, Self, TokenError<'src>>
86 where
87 I: TokenSource<'src>,
88 {
89 impl_block_properties_parser! {
90 known_properties: WorldProperty<'src> = {
91 p_id = key_value_numeric("id") => WorldProperty::Id,
92 p_mapversion = key_value_numeric("mapversion") => WorldProperty::MapVersion,
93 p_classname = key_value("classname") => |s: &str| WorldProperty::Classname(s),
94 p_detailmaterial = key_value("detailmaterial") => |s: &str| WorldProperty::DetailMaterial(s),
95 p_detailvbsp = key_value("detailvbsp") => |s: &str| WorldProperty::DetailVbsp(s),
96 p_maxpropscreenwidth = key_value_numeric("maxpropscreenwidth") => WorldProperty::MaxPropScreenWidth,
97 p_skyname = key_value("skyname") => |s: &str| WorldProperty::Skyname(s),
98 p_sounds = key_value_numeric("sounds") => WorldProperty::Sounds,
99 p_maxrange = key_value_numeric("maxrange") => WorldProperty::MaxRange,
100 p_maxoccludeearea = key_value_numeric("maxoccludeearea") => WorldProperty::MaxOccludeeArea,
101 p_minoccluderarea = key_value_numeric("minoccluderarea") => WorldProperty::MinOccluderArea,
102 p_maxoccludeearea_csgo = key_value_numeric("maxoccludeearea_csgo") => WorldProperty::MaxOccludeeAreaCsgo,
103 p_minoccluderarea_csgo = key_value_numeric("minoccluderarea_csgo") => WorldProperty::MinOccluderAreaCsgo,
104 p_difficulty_level = key_value_numeric("difficulty_level") => WorldProperty::DifficultyLevel,
105 p_hdr_level = key_value_numeric("hdr_level") => WorldProperty::HdrLevel,
106 p_targetname = key_value("targetname") => |s: &str| WorldProperty::Targetname(s),
107 p_target = key_value("target") => |s: &str| WorldProperty::Target(s),
108 p_hidden = key_value_boolean("hidden") => WorldProperty::Hidden,
109 p_group = Group::parser() => WorldProperty::Group,
110 }
111 }
112
113 let editor_parser = EditorData::parser().map(WorldProperty::Editor);
114 let solid_parser = Solid::parser().map(WorldProperty::Solid);
115 let custom_property = any_quoted_string()
116 .then(any_quoted_string())
117 .map(|(key, value): (&str, &str)| WorldProperty::Custom(key, value));
118
119 let any_property = known_properties
120 .or(editor_parser)
121 .or(solid_parser)
122 .or(custom_property);
123
124 open_block("world")
125 .ignore_then(
126 any_property
127 .repeated()
128 .collect::<Vec<WorldProperty<'src>>>(),
129 )
130 .then_ignore(close_block())
131 .map(|properties: Vec<WorldProperty<'src>>| {
132 let mut world = World::default();
133 for prop in properties {
134 match prop {
135 WorldProperty::Id(val) => world.id = val,
136 WorldProperty::MapVersion(val) => world.mapversion = val,
137 WorldProperty::Classname(val) => world.classname = val,
138 WorldProperty::DetailMaterial(val) => world.detailmaterial = Some(val),
139 WorldProperty::DetailVbsp(val) => world.detailvbsp = Some(val),
140 WorldProperty::MaxPropScreenWidth(val) => {
141 world.maxpropscreenwidth = Some(val)
142 }
143 WorldProperty::Skyname(val) => world.skyname = Some(val),
144 WorldProperty::Sounds(val) => world.sounds = Some(val),
145 WorldProperty::MaxRange(val) => world.maxrange = Some(val),
146 WorldProperty::MaxOccludeeArea(val) => world.maxoccludeearea = Some(val),
147 WorldProperty::MinOccluderArea(val) => world.minoccluderarea = Some(val),
148 WorldProperty::MaxOccludeeAreaCsgo(val) => {
149 world.maxoccludeearea_csgo = Some(val)
150 }
151 WorldProperty::MinOccluderAreaCsgo(val) => {
152 world.minoccluderarea_csgo = Some(val)
153 }
154 WorldProperty::DifficultyLevel(val) => world.difficulty_level = Some(val),
155 WorldProperty::HdrLevel(val) => world.hdr_level = Some(val),
156 WorldProperty::Targetname(val) => world.targetname = Some(val),
157 WorldProperty::Target(val) => world.target = Some(val),
158 WorldProperty::Hidden(val) => world.hidden = Some(val),
159 WorldProperty::Group(val) => world.group = Some(val),
160 WorldProperty::Editor(val) => world.editor = Some(val),
161 WorldProperty::Solid(val) => world.solids.push(val),
162 WorldProperty::Custom(key, value) => {
163 world.properties.insert(key, value);
164 }
165 }
166 }
167 world
168 })
169 .boxed()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::util::lex;
177
178 #[test]
179 fn test_world_minimal() {
180 let input = r#"
181 world
182 {
183 "id" "1"
184 "mapversion" "16"
185 "classname" "worldspawn"
186 }
187 "#;
188
189 let stream = lex(input);
190 let result = World::parse(stream);
191 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
192
193 let world = result.unwrap();
194 assert_eq!(world.id, 1);
195 assert_eq!(world.mapversion, 16);
196 assert_eq!(world.classname, "worldspawn");
197 assert_eq!(world.solids.len(), 0);
198 assert!(world.editor.is_none());
199 }
200
201 #[test]
202 fn test_world_with_properties() {
203 let input = r#"
204 world
205 {
206 "id" "1"
207 "mapversion" "16"
208 "classname" "worldspawn"
209 "detailmaterial" "detail/detailsprites"
210 "detailvbsp" "detail.vbsp"
211 "maxpropscreenwidth" "-1"
212 "skyname" "sky_day01_01"
213 }
214 "#;
215
216 let stream = lex(input);
217 let result = World::parse(stream);
218 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
219
220 let world = result.unwrap();
221 assert_eq!(world.detailmaterial, Some("detail/detailsprites"));
222 assert_eq!(world.detailvbsp, Some("detail.vbsp"));
223 assert_eq!(world.maxpropscreenwidth, Some(-1));
224 assert_eq!(world.skyname, Some("sky_day01_01"));
225 }
226
227 #[test]
228 fn test_world_with_solids() {
229 let input = r#"
230 world
231 {
232 "id" "1"
233 "mapversion" "16"
234 "classname" "worldspawn"
235 solid
236 {
237 "id" "9"
238 side
239 {
240 "id" "1"
241 "plane" "(0 0 0) (1 0 0) (1 1 0)"
242 "material" "DEV/DEV_MEASUREGENERIC01B"
243 "uaxis" "[1 0 0 0] 0.25"
244 "vaxis" "[0 -1 0 0] 0.25"
245 }
246 }
247 solid
248 {
249 "id" "30"
250 side
251 {
252 "id" "2"
253 "plane" "(0 0 0) (1 0 0) (1 1 0)"
254 "material" "DEV/DEV_MEASUREGENERIC01"
255 "uaxis" "[1 0 0 0] 0.25"
256 "vaxis" "[0 -1 0 0] 0.25"
257 }
258 }
259 }
260 "#;
261
262 let stream = lex(input);
263 let result = World::parse(stream);
264 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
265
266 let world = result.unwrap();
267 assert_eq!(world.solids.len(), 2);
268 assert_eq!(world.solids[0].id, 9);
269 assert_eq!(world.solids[1].id, 30);
270 }
271
272 #[test]
273 fn test_world_with_multiple_solids() {
274 let input = r#"
275 world
276 {
277 "id" "1"
278 "mapversion" "16"
279 "classname" "worldspawn"
280 "skyname" "sky_day01_01"
281 solid
282 {
283 "id" "1"
284 side
285 {
286 "id" "1"
287 "plane" "(0 0 0) (1 0 0) (1 1 0)"
288 "material" "CONCRETE"
289 "uaxis" "[1 0 0 0] 0.25"
290 "vaxis" "[0 -1 0 0] 0.25"
291 }
292 }
293 solid
294 {
295 "id" "2"
296 side
297 {
298 "id" "2"
299 "plane" "(0 0 0) (1 0 0) (1 1 0)"
300 "material" "METAL"
301 "uaxis" "[1 0 0 0] 0.25"
302 "vaxis" "[0 -1 0 0] 0.25"
303 }
304 }
305 solid
306 {
307 "id" "3"
308 side
309 {
310 "id" "3"
311 "plane" "(0 0 0) (1 0 0) (1 1 0)"
312 "material" "WOOD"
313 "uaxis" "[1 0 0 0] 0.25"
314 "vaxis" "[0 -1 0 0] 0.25"
315 }
316 }
317 }
318 "#;
319
320 let stream = lex(input);
321 let result = World::parse(stream);
322 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
323
324 let world = result.unwrap();
325 assert_eq!(world.solids.len(), 3);
326 assert_eq!(world.solids[0].id, 1);
327 assert_eq!(world.solids[1].id, 2);
328 assert_eq!(world.solids[2].id, 3);
329 }
330
331 #[test]
332 fn test_world_with_custom_properties() {
333 let input = r#"
334 world
335 {
336 "id" "1"
337 "mapversion" "16"
338 "classname" "worldspawn"
339 "customkey1" "customvalue1"
340 "customkey2" "customvalue2"
341 "_light" "255 255 255 200"
342 }
343 "#;
344
345 let stream = lex(input);
346 let result = World::parse(stream);
347 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
348
349 let world = result.unwrap();
350 assert_eq!(world.properties.get("customkey1"), Some(&"customvalue1"));
351 assert_eq!(world.properties.get("customkey2"), Some(&"customvalue2"));
352 assert_eq!(world.properties.get("_light"), Some(&"255 255 255 200"));
353 }
354
355 #[test]
356 fn test_world_properties_out_of_order() {
357 let input = r#"
358 world
359 {
360 "skyname" "sky_day01_01"
361 "classname" "worldspawn"
362 "id" "1"
363 "mapversion" "16"
364 "detailmaterial" "detail/detailsprites"
365 }
366 "#;
367
368 let stream = lex(input);
369 let result = World::parse(stream);
370 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
371
372 let world = result.unwrap();
373 assert_eq!(world.id, 1);
374 assert_eq!(world.mapversion, 16);
375 assert_eq!(world.classname, "worldspawn");
376 assert_eq!(world.skyname, Some("sky_day01_01"));
377 assert_eq!(world.detailmaterial, Some("detail/detailsprites"));
378 }
379
380 #[test]
381 fn test_world_with_editor() {
382 let input = r#"
383 world
384 {
385 "id" "1"
386 "mapversion" "16"
387 "classname" "worldspawn"
388 editor
389 {
390 "color" "255 0 0"
391 "visgroupshown" "1"
392 "visgroupautoshown" "1"
393 }
394 }
395 "#;
396
397 let stream = lex(input);
398 let result = World::parse(stream);
399 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
400
401 let world = result.unwrap();
402 assert!(world.editor.is_some());
403 let editor = world.editor.unwrap();
404 assert_eq!(editor.color.r, 255);
405 assert_eq!(editor.color.g, 0);
406 assert_eq!(editor.color.b, 0);
407 }
408
409 #[test]
410 fn test_world_with_game_specific_properties() {
411 let input = r#"
412 world
413 {
414 "id" "1"
415 "mapversion" "16"
416 "classname" "worldspawn"
417 "maxoccludeearea" "1000.5"
418 "minoccluderarea" "500.25"
419 "maxoccludeearea_csgo" "2000.75"
420 "minoccluderarea_csgo" "750.5"
421 "difficulty_level" "2"
422 "hdr_level" "1"
423 }
424 "#;
425
426 let stream = lex(input);
427 let result = World::parse(stream);
428 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
429
430 let world = result.unwrap();
431 assert_eq!(world.maxoccludeearea, Some(1000.5));
432 assert_eq!(world.minoccluderarea, Some(500.25));
433 assert_eq!(world.maxoccludeearea_csgo, Some(2000.75));
434 assert_eq!(world.minoccluderarea_csgo, Some(750.5));
435 assert_eq!(world.difficulty_level, Some(2));
436 assert_eq!(world.hdr_level, Some(1));
437 }
438
439 #[test]
440 fn test_world_with_entity_connections() {
441 let input = r#"
442 world
443 {
444 "id" "1"
445 "mapversion" "16"
446 "classname" "worldspawn"
447 "targetname" "world_spawn"
448 "target" "some_target"
449 }
450 "#;
451
452 let stream = lex(input);
453 let result = World::parse(stream);
454 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
455
456 let world = result.unwrap();
457 assert_eq!(world.targetname, Some("world_spawn"));
458 assert_eq!(world.target, Some("some_target"));
459 }
460
461 #[test]
462 fn test_world_with_editor_properties() {
463 let input = r#"
464 world
465 {
466 "id" "1"
467 "mapversion" "16"
468 "classname" "worldspawn"
469 "hidden" "1"
470 "group" "5"
471 }
472 "#;
473
474 let stream = lex(input);
475 let result = World::parse(stream);
476 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
477
478 let world = result.unwrap();
479 assert_eq!(world.hidden, Some(true));
480 }
481
482 #[test]
483 fn test_world_with_group_block() {
484 let input = r#"
485 world
486 {
487 "id" "1"
488 "classname" "worldspawn"
489 group
490 {
491 "id" "42"
492 group
493 {
494 "id" "99"
495 }
496 }
497 }
498 "#;
499
500 let stream = lex(input);
501 let result = World::parse(stream);
502 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
503
504 let world = result.unwrap();
505 let group = world.group.unwrap();
506 assert_eq!(group.id, 42);
507 assert_eq!(group.groups.len(), 1);
508 assert_eq!(group.groups[0].id, 99);
509 }
510
511 #[test]
512 fn test_world_empty_block() {
513 let input = r#"
514 world
515 {
516 }
517 "#;
518
519 let stream = lex(input);
520 let result = World::parse(stream);
521 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
522
523 let world = result.unwrap();
524 assert_eq!(world.id, 0);
525 assert_eq!(world.mapversion, 0);
526 assert_eq!(world.classname, "");
527 assert_eq!(world.solids.len(), 0);
528 }
529
530 #[test]
531 fn test_world_with_all_sound_properties() {
532 let input = r#"
533 world
534 {
535 "id" "1"
536 "mapversion" "16"
537 "classname" "worldspawn"
538 "sounds" "1"
539 "maxrange" "4096.5"
540 }
541 "#;
542
543 let stream = lex(input);
544 let result = World::parse(stream);
545 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
546
547 let world = result.unwrap();
548 assert_eq!(world.sounds, Some(1));
549 assert_eq!(world.maxrange, Some(4096.5));
550 }
551
552 #[test]
553 fn test_world_complete_with_everything() {
554 let input = r#"
555 world
556 {
557 "id" "1"
558 "mapversion" "16"
559 "classname" "worldspawn"
560 "detailmaterial" "detail/detailsprites"
561 "detailvbsp" "detail.vbsp"
562 "maxpropscreenwidth" "-1"
563 "skyname" "sky_day01_01"
564 solid
565 {
566 "id" "9"
567 side
568 {
569 "id" "1"
570 "plane" "(0 0 0) (1 0 0) (1 1 0)"
571 "material" "DEV/DEV_MEASUREGENERIC01B"
572 "uaxis" "[1 0 0 0] 0.25"
573 "vaxis" "[0 -1 0 0] 0.25"
574 }
575 editor
576 {
577 "color" "0 111 152"
578 "visgroupshown" "1"
579 "visgroupautoshown" "1"
580 }
581 }
582 editor
583 {
584 "color" "255 255 255"
585 "visgroupshown" "1"
586 "visgroupautoshown" "1"
587 }
588 }
589 "#;
590
591 let stream = lex(input);
592 let result = World::parse(stream);
593 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
594
595 let world = result.unwrap();
596 assert_eq!(world.id, 1);
597 assert_eq!(world.classname, "worldspawn");
598 assert_eq!(world.solids.len(), 1);
599 assert!(world.editor.is_some());
600 assert!(world.solids[0].editor.is_some());
601 }
602
603 #[test]
604 fn test_world_invalid_block_name() {
605 let input = r#"
606 wrongname
607 {
608 "id" "1"
609 "classname" "worldspawn"
610 }
611 "#;
612
613 let stream = lex(input);
614 let result = World::parse(stream);
615 assert!(result.is_err(), "Parser should fail on invalid block name");
616 }
617
618 #[test]
619 fn test_world_missing_closing_brace() {
620 let input = r#"
621 world
622 {
623 "id" "1"
624 "classname" "worldspawn"
625 "#;
626
627 let stream = lex(input);
628 let result = World::parse(stream);
629 assert!(
630 result.is_err(),
631 "Parser should fail on missing closing brace"
632 );
633 }
634
635 #[test]
636 fn test_world_mixed_solids_and_properties() {
637 let input = r#"
638 world
639 {
640 "id" "1"
641 solid
642 {
643 "id" "1"
644 side
645 {
646 "id" "1"
647 "plane" "(0 0 0) (1 0 0) (1 1 0)"
648 "material" "BRICK"
649 "uaxis" "[1 0 0 0] 0.25"
650 "vaxis" "[0 -1 0 0] 0.25"
651 }
652 }
653 "classname" "worldspawn"
654 "mapversion" "16"
655 solid
656 {
657 "id" "2"
658 side
659 {
660 "id" "2"
661 "plane" "(0 0 0) (1 0 0) (1 1 0)"
662 "material" "METAL"
663 "uaxis" "[1 0 0 0] 0.25"
664 "vaxis" "[0 -1 0 0] 0.25"
665 }
666 }
667 "skyname" "sky_day01_01"
668 }
669 "#;
670
671 let stream = lex(input);
672 let result = World::parse(stream);
673 assert!(result.is_ok(), "Parsing failed: {:?}", result.err());
674
675 let world = result.unwrap();
676 assert_eq!(world.id, 1);
677 assert_eq!(world.classname, "worldspawn");
678 assert_eq!(world.solids.len(), 2);
679 assert_eq!(world.skyname, Some("sky_day01_01"));
680 }
681}