Skip to main content

quake_map/
repr.rs

1#[cfg(feature = "std")]
2extern crate std;
3
4extern crate alloc;
5
6#[cfg(feature = "std")]
7use std::io;
8
9use {alloc::ffi::CString, alloc::format, alloc::vec::Vec, core::ffi::CStr};
10
11#[cfg(feature = "std")]
12use crate::{WriteError, WriteResult};
13
14use crate::{ValidationError, ValidationResult};
15
16/// Validation of entities and other items
17pub trait CheckWritable {
18    /// Determine if an item can be written to file
19    ///
20    /// Note that passing this check only implies that the item can be written
21    /// to file and can also be parsed back non-destructively.  It is up to the
22    /// consumer to ensure that the maps written are in a form that can be used
23    /// by other tools, e.g. qbsp.
24    fn check_writable(&self) -> ValidationResult;
25}
26
27/// 3-dimensional point used to determine the half-space a surface lies on
28pub type Point = [f64; 3];
29pub type Vec3 = [f64; 3];
30pub type Vec2 = [f64; 2];
31
32/// Transparent data structure representing a Quake source map
33///
34/// Contains a list of entities. Internal texture alignments may be in the
35/// original "legacy" Id format, the "Valve 220" format, or a mix of the two.
36#[derive(Clone, Debug)]
37pub struct QuakeMap {
38    pub entities: Vec<Entity>,
39}
40
41impl QuakeMap {
42    /// Instantiate a new map with 0 entities
43    pub const fn new() -> Self {
44        QuakeMap {
45            entities: Vec::new(),
46        }
47    }
48
49    /// Writes the map to the provided writer in text format, failing if
50    /// validation fails or an I/O error occurs
51    #[cfg(feature = "std")]
52    pub fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
53        for ent in &self.entities {
54            ent.write_to(writer)?;
55        }
56        Ok(())
57    }
58}
59
60impl Default for QuakeMap {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl CheckWritable for QuakeMap {
67    fn check_writable(&self) -> ValidationResult {
68        for ent in &self.entities {
69            ent.check_writable()?;
70        }
71
72        Ok(())
73    }
74}
75
76/// Entity type: `Brush` for entities with brushes, `Point` for entities without
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78pub enum EntityKind {
79    Point,
80    Brush,
81}
82
83/// A collection of key/value pairs in the form of an *edict* and 0 or more
84/// brushes
85#[derive(Clone, Debug)]
86pub struct Entity {
87    pub edict: Edict,
88    pub brushes: Vec<Brush>,
89}
90
91impl Entity {
92    /// Instantiate a new entity without any keyvalues or brushes
93    pub fn new() -> Self {
94        Entity {
95            edict: Edict::new(),
96            brushes: Vec::new(),
97        }
98    }
99
100    /// Determine whether this is a point or brush entity
101    pub fn kind(&self) -> EntityKind {
102        if self.brushes.is_empty() {
103            EntityKind::Point
104        } else {
105            EntityKind::Brush
106        }
107    }
108
109    #[cfg(feature = "std")]
110    pub fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
111        self.check_writable().map_err(WriteError::from)?;
112
113        writer.write_all(b"{\r\n").map_err(WriteError::Io)?;
114
115        write_edict_to(&self.edict, writer)?;
116
117        for brush in &self.brushes {
118            write_brush_to(brush, writer)?;
119        }
120
121        writer.write_all(b"}\r\n").map_err(WriteError::Io)?;
122        Ok(())
123    }
124}
125
126impl Default for Entity {
127    /// Same as `Entity::new`
128    fn default() -> Self {
129        Entity::new()
130    }
131}
132
133impl CheckWritable for Entity {
134    fn check_writable(&self) -> ValidationResult {
135        self.edict.check_writable()?;
136
137        for brush in &self.brushes {
138            brush.check_writable()?
139        }
140
141        Ok(())
142    }
143}
144
145/// Entity dictionary
146pub type Edict = Vec<(CString, CString)>;
147
148impl CheckWritable for Edict {
149    fn check_writable(&self) -> ValidationResult {
150        for (k, v) in self {
151            check_writable_quoted(k)?;
152            check_writable_quoted(v)?;
153        }
154
155        Ok(())
156    }
157}
158
159/// Convex polyhedron
160pub type Brush = Vec<Surface>;
161
162impl CheckWritable for Brush {
163    fn check_writable(&self) -> ValidationResult {
164        for surface in self {
165            surface.check_writable()?;
166        }
167
168        Ok(())
169    }
170}
171
172/// Brush face
173///
174/// Set `q2ext` to its default (`Default::default()`) value to create a surface
175/// compatible for Quake 1 tools
176#[derive(Clone, Debug)]
177pub struct Surface {
178    pub half_space: HalfSpace,
179    pub texture: CString,
180    pub alignment: Alignment,
181    pub q2ext: Quake2SurfaceExtension,
182}
183
184impl Surface {
185    #[cfg(feature = "std")]
186    fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
187        write_half_space_to(&self.half_space, writer)?;
188        writer.write_all(b" ").map_err(WriteError::Io)?;
189        write_texture_to(&self.texture, writer)?;
190        writer.write_all(b" ").map_err(WriteError::Io)?;
191        self.alignment.write_to(writer)?;
192
193        if !self.q2ext.is_zeroed() {
194            writer.write_all(b" ").map_err(WriteError::Io)?;
195            self.q2ext.write_to(writer)?;
196        }
197
198        Ok(())
199    }
200}
201
202impl CheckWritable for Surface {
203    fn check_writable(&self) -> ValidationResult {
204        self.half_space.check_writable()?;
205        check_writable_texture(&self.texture)?;
206        self.alignment.check_writable()
207    }
208}
209
210/// A set of 3 points that determine a plane with its facing direction
211/// determined by the winding order of the points
212pub type HalfSpace = [Point; 3];
213
214impl CheckWritable for HalfSpace {
215    fn check_writable(&self) -> ValidationResult {
216        for num in self.iter().flatten() {
217            check_writable_f64(*num)?;
218        }
219
220        Ok(())
221    }
222}
223
224/// Texture alignment properties
225///
226/// If axes are present, the alignment will be written in the "Valve220" format;
227/// otherwise it will be written in the "legacy" format pre-dating Valve220.
228#[derive(Clone, Copy, Debug)]
229pub struct Alignment {
230    pub offset: Vec2,
231    pub rotation: f64,
232    pub scale: Vec2,
233
234    /// Describes the X and Y texture-space axes
235    pub axes: Option<[Vec3; 2]>,
236}
237
238impl Alignment {
239    #[cfg(feature = "std")]
240    fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
241        match self.axes {
242            None => {
243                write!(
244                    writer,
245                    "{} {} {} {} {}",
246                    self.offset[0],
247                    self.offset[1],
248                    self.rotation,
249                    self.scale[0],
250                    self.scale[1]
251                )
252                .map_err(WriteError::Io)?;
253            }
254            Some([u, v]) => {
255                write!(
256                    writer,
257                    "[ {} {} {} {} ] [ {} {} {} {} ] {} {} {}",
258                    u[0],
259                    u[1],
260                    u[2],
261                    self.offset[0],
262                    v[0],
263                    v[1],
264                    v[2],
265                    self.offset[1],
266                    self.rotation,
267                    self.scale[0],
268                    self.scale[1]
269                )
270                .map_err(WriteError::Io)?;
271            }
272        }
273        Ok(())
274    }
275}
276
277/// Quake II Surface Extension
278///
279/// Additional fields for surfaces to support Quake II maps.  Contains two
280/// bit fields (up to 31 bits in each; negative values are non-standard, but
281/// a signed type is used for consistency with existing tools) and a floating-
282/// point value (_ought_ to be an integer, but TrenchBroom allows writing
283/// floats).
284#[derive(Clone, Copy, Debug)]
285pub struct Quake2SurfaceExtension {
286    /// Flags describing contents of the brush
287    pub content_flags: i32,
288
289    /// Flags describing the surface
290    pub surface_flags: i32,
291
292    /// Value associated with surface, e.g. light value for emissive surfaces
293    pub surface_value: f64,
294}
295
296impl Quake2SurfaceExtension {
297    /// Returns true if all fields are 0, otherwise false
298    pub fn is_zeroed(&self) -> bool {
299        self.content_flags == 0
300            && self.surface_flags == 0
301            && self.surface_value == 0.0
302    }
303
304    #[cfg(feature = "std")]
305    fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
306        write!(
307            writer,
308            "{} {} {}",
309            self.content_flags, self.surface_flags, self.surface_value,
310        )
311        .map_err(WriteError::Io)?;
312
313        Ok(())
314    }
315}
316
317impl Default for Quake2SurfaceExtension {
318    fn default() -> Self {
319        Self {
320            content_flags: 0,
321            surface_flags: 0,
322            surface_value: 0.0,
323        }
324    }
325}
326
327impl CheckWritable for Alignment {
328    fn check_writable(&self) -> ValidationResult {
329        check_writable_array(self.offset)?;
330        check_writable_f64(self.rotation)?;
331        check_writable_array(self.scale)?;
332
333        if let Some(axes) = self.axes {
334            for axis in axes {
335                check_writable_array(axis)?;
336            }
337        }
338
339        Ok(())
340    }
341}
342
343#[cfg(feature = "std")]
344fn write_edict_to<W: io::Write>(edict: &Edict, writer: &mut W) -> WriteResult {
345    for (key, value) in edict {
346        writer.write_all(b"\"").map_err(WriteError::Io)?;
347        writer.write_all(key.as_bytes()).map_err(WriteError::Io)?;
348        writer.write_all(b"\" \"").map_err(WriteError::Io)?;
349        writer.write_all(value.as_bytes()).map_err(WriteError::Io)?;
350        writer.write_all(b"\"\r\n").map_err(WriteError::Io)?;
351    }
352    Ok(())
353}
354
355#[cfg(feature = "std")]
356fn write_brush_to<W: io::Write>(brush: &Brush, writer: &mut W) -> WriteResult {
357    writer.write_all(b"{\r\n").map_err(WriteError::Io)?;
358
359    for surf in brush {
360        surf.write_to(writer)?;
361        writer.write_all(b"\r\n").map_err(WriteError::Io)?;
362    }
363
364    writer.write_all(b"}\r\n").map_err(WriteError::Io)?;
365    Ok(())
366}
367
368#[cfg(feature = "std")]
369fn write_half_space_to<W: io::Write>(
370    half_space: &HalfSpace,
371    writer: &mut W,
372) -> WriteResult {
373    for (index, pt) in half_space.iter().enumerate() {
374        writer.write_all(b"( ").map_err(WriteError::Io)?;
375
376        for element in pt.iter() {
377            write!(writer, "{} ", element).map_err(WriteError::Io)?;
378        }
379
380        writer.write_all(b")").map_err(WriteError::Io)?;
381
382        if index != 2 {
383            writer.write_all(b" ").map_err(WriteError::Io)?;
384        }
385    }
386    Ok(())
387}
388
389#[cfg(feature = "std")]
390fn write_texture_to<W: io::Write>(
391    texture: &CStr,
392    writer: &mut W,
393) -> WriteResult {
394    let needs_quotes =
395        texture.to_bytes().iter().any(|c| c.is_ascii_whitespace())
396            || texture.to_bytes().is_empty();
397
398    if needs_quotes {
399        writer.write_all(b"\"").map_err(WriteError::Io)?;
400    }
401
402    writer
403        .write_all(texture.to_bytes())
404        .map_err(WriteError::Io)?;
405
406    if needs_quotes {
407        writer.write_all(b"\"").map_err(WriteError::Io)?;
408    }
409
410    Ok(())
411}
412
413fn check_writable_array<const N: usize>(arr: [f64; N]) -> ValidationResult {
414    for num in arr {
415        check_writable_f64(num)?;
416    }
417
418    Ok(())
419}
420
421fn check_writable_f64(num: f64) -> ValidationResult {
422    if num.is_finite() {
423        Ok(())
424    } else {
425        Err(format!("Non-finite number ({})", num).into())
426    }
427}
428
429fn check_writable_texture(s: &CStr) -> ValidationResult {
430    if check_writable_unquoted(s).is_ok() {
431        return Ok(());
432    }
433
434    match check_writable_quoted(s) {
435        Ok(_) => Ok(()),
436        Err(_) => Err(format!(
437            "Cannot write texture {:?}, not quotable and contains whitespace",
438            s
439        )
440        .into()),
441    }
442}
443
444fn check_writable_quoted(s: &CStr) -> ValidationResult {
445    let bad_chars = [b'"', b'\r', b'\n'];
446
447    for c in s.to_bytes() {
448        if bad_chars.contains(c) {
449            return Err(format!(
450                "Cannot write quote-wrapped string, contains {:?}",
451                char::from(*c)
452            )
453            .into());
454        }
455    }
456
457    Ok(())
458}
459
460fn check_writable_unquoted(s: &CStr) -> ValidationResult {
461    let s_bytes = s.to_bytes();
462
463    if s_bytes.is_empty() {
464        return Err(ValidationError::from(
465            "Cannot write unquoted empty string",
466        ));
467    }
468
469    if s_bytes[0] == b'"' {
470        return Err(ValidationError::from(
471            "Cannot lead unquoted string with quote",
472        ));
473    }
474
475    if contains_ascii_whitespace(s) {
476        Err(ValidationError::from(
477            "Cannot write unquoted string, contains whitespace",
478        ))
479    } else {
480        Ok(())
481    }
482}
483
484fn contains_ascii_whitespace(s: &CStr) -> bool {
485    s.to_bytes().iter().any(|c| c.is_ascii_whitespace())
486}