quake_util/qmap/
repr.rs

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