Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quake-util"
version = "0.3.2"
version = "0.4.0"
authors = ["Seth Rader"]
edition = "2021"
description = "A utility library for using Quake file formats"
Expand Down
8 changes: 8 additions & 0 deletions examples/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Changelog

### 0.4.0

* Implemented support for reading & writing Quake II map files

* Replaced `HashMap<CString, CString>` with `Vec<(CString, CString)>` for entity
key/values (breaking)
7 changes: 7 additions & 0 deletions src/qmap/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ impl Token {
&& self.text.last() == Some(&b'"'.try_into().unwrap())
}

pub fn starts_numeric(&self) -> bool {
!self.text.is_empty() && {
let first_byte = self.text[0].get();
first_byte == b'-' || first_byte.is_ascii_digit()
}
}

pub fn text_as_string(&self) -> String {
self.text
.iter()
Expand Down
50 changes: 45 additions & 5 deletions src/qmap/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::{io::Read, iter::Peekable, num::NonZeroU8, str::FromStr, vec::Vec};
use crate::{common, qmap, TextParseError, TextParseResult};
use common::CellOptionExt;
use qmap::lexer::{Token, TokenIterator};
use qmap::repr::{Alignment, Brush, Edict, Entity, Point, QuakeMap, Surface};
use qmap::repr::{
Alignment, Brush, Edict, Entity, Point, Quake2SurfaceExtension, QuakeMap,
Surface,
};

type TokenPeekable<R> = Peekable<TokenIterator<R>>;

Expand All @@ -26,10 +29,9 @@ const MIN_BRUSH_SURFACES: usize = 4;

/// Parses a Quake source map
///
/// Maps must be in the Quake 1 format (Quake 2 surface flags and Quake 3
/// `brushDef`s/`patchDef`s are not presently supported) but may have texture
/// alignment in either "Valve220" format or the "legacy" predecessor (i.e.
/// without texture axes)
/// Maps must be in the Quake 1 or 2 format (Quake 3 `brushDef`s/`patchDef`s are
/// not presently supported) but may have texture alignment in either "Valve220"
/// format or the "legacy" predecessor (i.e. without texture axes)
pub fn parse<R: Read>(reader: &mut R) -> TextParseResult<QuakeMap> {
let mut entities: Vec<Entity> = Vec::new();
let mut peekable_tokens = TokenIterator::new(reader).peekable();
Expand Down Expand Up @@ -143,10 +145,21 @@ fn parse_surface<R: Read>(
return Err(TextParseError::eof());
};

let q2ext = if let Some(tok_res) = tokens.peek() {
if tok_res.as_ref().map_err(|e| e.steal())?.starts_numeric() {
parse_q2_ext(tokens)?
} else {
Default::default()
}
} else {
return Err(TextParseError::eof());
};

Ok(Surface {
half_space,
texture,
alignment,
q2ext,
})
}

Expand Down Expand Up @@ -179,6 +192,20 @@ fn parse_legacy_alignment<R: Read>(
})
}

fn parse_q2_ext<R: Read>(
tokens: &mut TokenPeekable<R>,
) -> TextParseResult<Quake2SurfaceExtension> {
let content_flags = expect_int(&tokens.extract()?)?;
let surface_flags = expect_int(&tokens.extract()?)?;
let surface_value = expect_float(&tokens.extract()?)?;

Ok(Quake2SurfaceExtension {
content_flags,
surface_flags,
surface_value,
})
}

fn parse_valve_alignment<R: Read>(
tokens: &mut TokenPeekable<R>,
) -> TextParseResult<Alignment> {
Expand Down Expand Up @@ -276,6 +303,19 @@ fn expect_float(token: &Option<Token>) -> TextParseResult<f64> {
}
}

fn expect_int(token: &Option<Token>) -> TextParseResult<i32> {
match token.as_ref() {
Some(payload) => match i32::from_str(&payload.text_as_string()) {
Ok(num) => Ok(num),
Err(_) => Err(TextParseError::from_parser(
format!("Expected integer, got `{}`", payload.text_as_string()),
payload.line_number,
)),
},
None => Err(TextParseError::eof()),
}
}

fn strip_quoted(quoted_text: &[NonZeroU8]) -> &[NonZeroU8] {
&quoted_text[1..quoted_text.len() - 1]
}
55 changes: 51 additions & 4 deletions src/qmap/parser_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn parse_standard_brush_entity() {
assert_eq!(surface.alignment.offset, [0.0, 0.0]);
assert_eq!(surface.alignment.rotation, 0.0);
assert_eq!(surface.alignment.scale, [1.0, 1.0]);
assert!(surface.q2ext.is_zeroed());
}

#[test]
Expand Down Expand Up @@ -127,6 +128,7 @@ fn parse_valve_brush_entity() {
surface.alignment.axes,
Some([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]),
);
assert!(surface.q2ext.is_zeroed());
}

#[test]
Expand Down Expand Up @@ -189,6 +191,35 @@ fn parse_weird_textures() {
assert_eq!(surface3.texture, CString::new(r#"silly"example"#).unwrap());
}

#[test]
fn parse_q2_surface() {
let map = parse(
&mut &br#"
{ {
( 1 2 3 ) ( 4 5 6 ) ( 7 8 9 )
T
0 0 0 1 1 -97 9 255

( 2 5 7 ) ( 8 9 7 ) ( 4 3 6 )
T
0 0 0 1 1 0 -9 -13.7
} }
"#[..],
)
.unwrap();

let std_ext = &map.entities[0].brushes[0][0].q2ext;
let valve_ext = &map.entities[0].brushes[0][1].q2ext;
assert!(!std_ext.is_zeroed());
assert_eq!(std_ext.content_flags, -97);
assert_eq!(std_ext.surface_flags, 9);
assert_eq!(std_ext.surface_value, 255.0);
assert!(!valve_ext.is_zeroed());
assert_eq!(valve_ext.content_flags, 0);
assert_eq!(valve_ext.surface_flags, -9);
assert_eq!(valve_ext.surface_value, -13.7);
}

// Parse errors

#[test]
Expand Down Expand Up @@ -269,10 +300,26 @@ fn parse_unclosed_surface() {
{
"#;
let err = parse(&mut &map_text[..]).err().unwrap();
if let error::TextParse::Parser(line_err) = err {
let (pfx, _) = line_err.message.split_once("got").unwrap();
assert!(pfx.contains("`}`"));
assert!(pfx.contains("`(`"));
if let error::TextParse::Parser(ref line_err) = err {
assert!(line_err.message.contains("`}`"));
assert!(line_err.message.contains("`(`"));
} else {
panic_unexpected_variant(err);
}
}

#[test]
fn parse_incomplete_q2_extension() {
let map_text = br#"
{ {
( 1 2 3 ) ( 4 5 6 ) ( 7 8 9 ) t 0 0 0 1 1 -2
} }
"#;

let err = parse(&mut &map_text[..]).err().unwrap();
if let error::TextParse::Parser(ref line_err) = err {
assert!(line_err.message.contains("integer"));
assert!(line_err.message.contains("`}`"));
} else {
panic_unexpected_variant(err);
}
Expand Down
62 changes: 62 additions & 0 deletions src/qmap/repr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,15 @@ impl CheckWritable for Brush {
}

/// Brush face
///
/// Set `q2ext` to its default (`Default::default()`) value to create a surface
/// compatible for Quake 1 tools
#[derive(Clone, Debug)]
pub struct Surface {
pub half_space: HalfSpace,
pub texture: CString,
pub alignment: Alignment,
pub q2ext: Quake2SurfaceExtension,
}

impl Surface {
Expand All @@ -189,6 +193,12 @@ impl Surface {
write_texture_to(&self.texture, writer)?;
writer.write_all(b" ").map_err(WriteError::Io)?;
self.alignment.write_to(writer)?;

if !self.q2ext.is_zeroed() {
writer.write_all(b" ").map_err(WriteError::Io)?;
self.q2ext.write_to(writer)?;
}

Ok(())
}
}
Expand Down Expand Up @@ -268,6 +278,58 @@ impl Alignment {
}
}

/// Quake II Surface Extension
///
/// Additional fields for surfaces to support Quake II maps. Contains two
/// bit fields (up to 31 bits in each; negative values are non-standard, but
/// a signed type is used for consistency with existing tools) and a floating-
/// point value (_ought_ to be an integer, but TrenchBroom allows writing
/// floats).
#[derive(Clone, Copy, Debug)]
pub struct Quake2SurfaceExtension {
/// Flags describing contents of the brush
pub content_flags: i32,

/// Flags describing the surface
pub surface_flags: i32,

/// Value associated with surface, e.g. light value for emissive surfaces
pub surface_value: f64,
}

impl Quake2SurfaceExtension {
/// Returns true if all fields are 0, otherwise false. Behavior is
/// undefined if `surface_value` is NaN (read: behavior may change between
/// revisions without remark).
pub fn is_zeroed(&self) -> bool {
self.content_flags == 0
&& self.surface_flags == 0
&& self.surface_value == 0.0
}

#[cfg(feature = "std")]
fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteAttempt {
write!(
writer,
"{} {} {}",
self.content_flags, self.surface_flags, self.surface_value,
)
.map_err(WriteError::Io)?;

Ok(())
}
}

impl Default for Quake2SurfaceExtension {
fn default() -> Self {
Self {
content_flags: 0,
surface_flags: 0,
surface_value: 0.0,
}
}
}

impl CheckWritable for Alignment {
fn check_writable(&self) -> ValidationResult {
check_writable_array(self.offset)?;
Expand Down
Loading
Loading