lib_curveball/map/
qmap.rs

1// Copyright 2025 Jordan Johnson
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Structs for Quake3 maps.
5
6use crate::map::geometry::Brush;
7use core::fmt;
8use std::fmt::{Display, Formatter};
9
10use std::collections::HashMap;
11
12/// A Quake3 map entity.
13#[derive(Debug, Clone)]
14pub struct QEntity {
15    pub parameters: HashMap<String, String>,
16    pub brushes: Vec<Brush>,
17}
18
19impl QEntity {
20    pub(crate) fn bake(&self) -> impl Display + use<'_> {
21        struct QEntityDisp<'a>(&'a QEntity);
22        impl Display for QEntityDisp<'_> {
23            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
24                writeln!(f, "{{",)?;
25                for (key, value) in self.0.parameters.iter() {
26                    writeln!(f, "\"{key}\" \"{value}\"")?;
27                }
28                for (i, brush) in self.0.brushes.iter().enumerate() {
29                    writeln!(f, "// brush {i}")?;
30                    write!(f, "{}", brush.bake())?;
31                }
32                writeln!(f, "}}")?;
33                Ok(())
34            }
35        }
36        QEntityDisp(self)
37    }
38}
39
40/// A Quake3 map.
41///
42/// Importantly, this struct implements `Display`, so one can convert this struct into text to be
43/// written to a `.map` file.
44#[derive(Debug, Clone)]
45pub struct QMap {
46    pub entities: Vec<QEntity>,
47    pub metadata: Vec<String>,
48}
49
50impl QMap {
51    pub fn new(entities: Vec<QEntity>) -> Self {
52        Self {
53            entities,
54            metadata: Vec::new(),
55        }
56    }
57
58    pub fn with_metadata(mut self, metadata: String) -> Self {
59        self.metadata.push(metadata);
60        self
61    }
62
63    pub fn with_tb_neverball_metadata(self) -> Self {
64        self.with_metadata("Game: Neverball".to_string())
65            .with_metadata("Format: Quake3".to_string())
66    }
67
68    pub fn bake(&self) -> impl Display + use<'_> {
69        struct QMapDisp<'a>(&'a QMap);
70        impl Display for QMapDisp<'_> {
71            fn fmt(&self, f: &mut Formatter) -> fmt::Result {
72                for metadata_line in self.0.metadata.iter().flat_map(|meta| meta.lines()) {
73                    writeln!(f, "// {}", metadata_line)?;
74                }
75                for (i, entity) in self.0.entities.iter().enumerate() {
76                    writeln!(f, "// entity {i}")?;
77                    write!(f, "{}", entity.bake())?;
78                }
79                Ok(())
80            }
81        }
82        QMapDisp(self)
83    }
84}
85
86impl Display for QMap {
87    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
88        write!(f, "{}", self.bake())
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::map::entity::SimpleWorldspawn;
96    use glam::DVec3;
97
98    #[test]
99    fn compile_map() {
100        let vertices = vec![
101            DVec3::from([0.0, 0.0, 0.0]),
102            DVec3::from([0.0, 0.0, 1.0]),
103            DVec3::from([0.0, 1.0, 0.0]),
104            DVec3::from([1.0, 0.0, 0.0]),
105            DVec3::from([0.3, 0.3, 0.3]),
106        ];
107
108        let brush1 = Brush::try_from_vertices(&vertices, Some(1000)).unwrap();
109        let brush2 = Brush::try_from_vertices(&vertices, Some(1000)).unwrap();
110
111        let worldspawn = SimpleWorldspawn::new(vec![brush1, brush2]);
112        let entity: QEntity = worldspawn.into();
113        let map: QMap = QMap::new(vec![entity]).with_tb_neverball_metadata();
114
115        println!("{}", map.to_string());
116
117        let should_eq_str = r#"// Game: Neverball
118// Format: Quake3
119// entity 0
120{
121"classname" "worldspawn"
122// brush 0
123{
124( 0.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
125( 0.000000 1.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
126( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
127( 0.000000 0.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
128}
129// brush 1
130{
131( 0.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
132( 0.000000 1.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
133( 1.000000 0.000000 0.000000 ) ( 0.000000 0.000000 0.000000 ) ( 0.000000 0.000000 1.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
134( 0.000000 0.000000 0.000000 ) ( 1.000000 0.000000 0.000000 ) ( 0.000000 1.000000 0.000000 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0
135}
136}
137"#;
138        println!("{}", should_eq_str);
139        assert_eq!(format!("{}", map.to_string()), should_eq_str);
140    }
141}