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.  Behavior is
298    /// undefined if `surface_value` is NaN (read: behavior may change between
299    /// revisions without remark).
300    pub fn is_zeroed(&self) -> bool {
301        self.content_flags == 0
302            && self.surface_flags == 0
303            && self.surface_value == 0.0
304    }
305
306    #[cfg(feature = "std")]
307    fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteResult {
308        write!(
309            writer,
310            "{} {} {}",
311            self.content_flags, self.surface_flags, self.surface_value,
312        )
313        .map_err(WriteError::Io)?;
314
315        Ok(())
316    }
317}
318
319impl Default for Quake2SurfaceExtension {
320    fn default() -> Self {
321        Self {
322            content_flags: 0,
323            surface_flags: 0,
324            surface_value: 0.0,
325        }
326    }
327}
328
329impl CheckWritable for Alignment {
330    fn check_writable(&self) -> ValidationResult {
331        check_writable_array(self.offset)?;
332        check_writable_f64(self.rotation)?;
333        check_writable_array(self.scale)?;
334
335        if let Some(axes) = self.axes {
336            for axis in axes {
337                check_writable_array(axis)?;
338            }
339        }
340
341        Ok(())
342    }
343}
344
345#[cfg(feature = "std")]
346fn write_edict_to<W: io::Write>(edict: &Edict, writer: &mut W) -> WriteResult {
347    for (key, value) in edict {
348        writer.write_all(b"\"").map_err(WriteError::Io)?;
349        writer.write_all(key.as_bytes()).map_err(WriteError::Io)?;
350        writer.write_all(b"\" \"").map_err(WriteError::Io)?;
351        writer.write_all(value.as_bytes()).map_err(WriteError::Io)?;
352        writer.write_all(b"\"\r\n").map_err(WriteError::Io)?;
353    }
354    Ok(())
355}
356
357#[cfg(feature = "std")]
358fn write_brush_to<W: io::Write>(brush: &Brush, writer: &mut W) -> WriteResult {
359    writer.write_all(b"{\r\n").map_err(WriteError::Io)?;
360
361    for surf in brush {
362        surf.write_to(writer)?;
363        writer.write_all(b"\r\n").map_err(WriteError::Io)?;
364    }
365
366    writer.write_all(b"}\r\n").map_err(WriteError::Io)?;
367    Ok(())
368}
369
370#[cfg(feature = "std")]
371fn write_half_space_to<W: io::Write>(
372    half_space: &HalfSpace,
373    writer: &mut W,
374) -> WriteResult {
375    for (index, pt) in half_space.iter().enumerate() {
376        writer.write_all(b"( ").map_err(WriteError::Io)?;
377
378        for element in pt.iter() {
379            write!(writer, "{} ", element).map_err(WriteError::Io)?;
380        }
381
382        writer.write_all(b")").map_err(WriteError::Io)?;
383
384        if index != 2 {
385            writer.write_all(b" ").map_err(WriteError::Io)?;
386        }
387    }
388    Ok(())
389}
390
391#[cfg(feature = "std")]
392fn write_texture_to<W: io::Write>(
393    texture: &CStr,
394    writer: &mut W,
395) -> WriteResult {
396    let needs_quotes =
397        texture.to_bytes().iter().any(|c| c.is_ascii_whitespace())
398            || texture.to_bytes().is_empty();
399
400    if needs_quotes {
401        writer.write_all(b"\"").map_err(WriteError::Io)?;
402    }
403
404    writer
405        .write_all(texture.to_bytes())
406        .map_err(WriteError::Io)?;
407
408    if needs_quotes {
409        writer.write_all(b"\"").map_err(WriteError::Io)?;
410    }
411
412    Ok(())
413}
414
415fn check_writable_array<const N: usize>(arr: [f64; N]) -> ValidationResult {
416    for num in arr {
417        check_writable_f64(num)?;
418    }
419
420    Ok(())
421}
422
423fn check_writable_f64(num: f64) -> ValidationResult {
424    if num.is_finite() {
425        Ok(())
426    } else {
427        Err(format!("Non-finite number ({})", num).into())
428    }
429}
430
431fn check_writable_texture(s: &CStr) -> ValidationResult {
432    if check_writable_unquoted(s).is_ok() {
433        return Ok(());
434    }
435
436    match check_writable_quoted(s) {
437        Ok(_) => Ok(()),
438        Err(_) => Err(format!(
439            "Cannot write texture {:?}, not quotable and contains whitespace",
440            s
441        )
442        .into()),
443    }
444}
445
446fn check_writable_quoted(s: &CStr) -> ValidationResult {
447    let bad_chars = [b'"', b'\r', b'\n'];
448
449    for c in s.to_bytes() {
450        if bad_chars.contains(c) {
451            return Err(format!(
452                "Cannot write quote-wrapped string, contains {:?}",
453                char::from(*c)
454            )
455            .into());
456        }
457    }
458
459    Ok(())
460}
461
462fn check_writable_unquoted(s: &CStr) -> ValidationResult {
463    let s_bytes = s.to_bytes();
464
465    if s_bytes.is_empty() {
466        return Err(ValidationError::from(
467            "Cannot write unquoted empty string",
468        ));
469    }
470
471    if s_bytes[0] == b'"' {
472        return Err(ValidationError::from(
473            "Cannot lead unquoted string with quote",
474        ));
475    }
476
477    if contains_ascii_whitespace(s) {
478        Err(ValidationError::from(
479            "Cannot write unquoted string, contains whitespace",
480        ))
481    } else {
482        Ok(())
483    }
484}
485
486fn contains_ascii_whitespace(s: &CStr) -> bool {
487    s.to_bytes().iter().any(|c| c.is_ascii_whitespace())
488}