Skip to content

Commit

Permalink
Merge pull request #22 from 4LT/docs
Browse files Browse the repository at this point in the history
Docs
  • Loading branch information
4LT authored Mar 18, 2023
2 parents cdee91f + 5233b7f commit 013ef62
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
[package]
name = "quake-util"
version = "0.1.1"
version = "0.2.0"
authors = ["Seth <[email protected]>"]
edition = "2021"
description = "A utility library for using Quake file formats"
license = "CC0-1.0 OR MIT OR Apache-2.0"
documentation = "https://docs.rs/quake-util"

[features]
default = ["std"]
Expand Down
58 changes: 55 additions & 3 deletions src/qmap/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,65 @@
pub mod repr;
//! Quake source map data structures, parsing, and writing
//!
//! # Example
//!
//! ```
//! # use quake_util::qmap;
//! # use std::ffi::CString;
//! # use std::io::Read;
//! #
//! #
//! # fn main() -> Result<(), String> {
//! # #[cfg(feature="std")]
//! # {
//! # let source = &b"
//! # {
//! # }
//! # "[..];
//! #
//! # let mut dest = Vec::<u8>::new();
//! #
//! use qmap::{Entity, ParseError, WriteError};
//!
//! let mut map = qmap::parse(source).map_err(|err| match err {
//! ParseError::Io(_) => String::from("Failed to read map"),
//! l_err@ParseError::Lexer(_) => l_err.to_string(),
//! p_err@ParseError::Parser(_) => p_err.to_string(),
//! })?;
//!
//! let mut soldier = Entity::new();
//!
//! soldier.edict.insert(
//! CString::new("classname").unwrap(),
//! CString::new("monster_army").unwrap(),
//! );
//!
//! soldier.edict.insert(
//! CString::new("origin").unwrap(),
//! CString::new("128 -256 24").unwrap(),
//! );
//!
//! map.entities.push(soldier);
//!
//! map.write_to(&mut dest).map_err(|err| match err {
//! WriteError::Io(e) => e.to_string(),
//! WriteError::Validation(msg) => msg
//! })?;
//! #
//! # }
//! # Ok(())
//! # }
//! ```
mod repr;

#[cfg(feature = "std")]
mod lexer;

#[cfg(feature = "std")]
pub mod parser;
mod parser;

#[cfg(feature = "std")]
pub mod parse_result;
mod parse_result;

pub use repr::{
Alignment, Brush, CheckWritable, Edict, Entity, EntityKind, HalfSpace,
Expand Down
37 changes: 36 additions & 1 deletion src/qmap/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ use qmap::ParseResult;
type TokenPeekable<R> = Peekable<TokenIterator<R>>;
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)
pub fn parse<R: Read>(reader: R) -> ParseResult<QuakeMap> {
let mut entities: Vec<Entity> = Vec::new();
let mut peekable_tokens = TokenIterator::new(reader).peekable();
Expand Down Expand Up @@ -83,7 +89,7 @@ fn parse_brush<R: Read>(tokens: &mut TokenPeekable<R>) -> ParseResult<Brush> {
}
}

expect_byte(&tokens.next().transpose()?, b'}')?;
expect_byte_or(&tokens.next().transpose()?, b'}', &[b'('])?;
Ok(surfaces)
}

Expand Down Expand Up @@ -195,6 +201,35 @@ fn expect_byte(token: &Option<Token>, byte: u8) -> ParseResult<()> {
}
}

fn expect_byte_or(
token: &Option<Token>,
byte: u8,
rest: &[u8],
) -> ParseResult<()> {
match token.as_ref() {
Some(payload) if payload.match_byte(byte) => Ok(()),
Some(payload) => {
let rest_str = (&rest
.iter()
.copied()
.map(|b| format!("`{}`", char::from(b)))
.collect::<Vec<_>>()[..])
.join(", ");

Err(qmap::ParseError::from_parser(
format!(
"Expected {} or `{}`, got `{}`",
rest_str,
char::from(byte),
payload.text_as_string()
),
payload.line_number,
))
}
_ => Err(qmap::ParseError::eof()),
}
}

fn expect_quoted(token: &Option<Token>) -> ParseResult<()> {
match token.as_ref() {
Some(payload) if payload.match_quoted() => Ok(()),
Expand Down
19 changes: 19 additions & 0 deletions src/qmap/parser_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,22 @@ fn parse_bad_texture_name() {
panic_unexpected_variant(err);
}
}

#[test]
fn parse_unclosed_surface() {
let map_text = br#"
{
"classname" "world"
{
( 1 2 3 ) ( 2 3 1 ) ( 3 1 2 ) tex 0 0 0 1 1
{
"#;
let err = parse(&map_text[..]).err().unwrap();
if let ParseError::Parser(line_err) = err {
let (pfx, _) = line_err.message.split_once("got").unwrap();
assert!(pfx.contains("`}`"));
assert!(pfx.contains("`(`"));
} else {
panic_unexpected_variant(err);
}
}
33 changes: 33 additions & 0 deletions src/qmap/repr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ use {
core::ffi::CStr, hashbrown::HashMap,
};

/// Return type for validating writability of entities and other items
pub type ValidationResult = Result<(), String>;

/// Validation of entities and other items
pub trait CheckWritable {
/// Determine if an item can be written to file
///
/// Note that passing this check only implies that the item can be written
/// to file and can also be parsed back non-destructively. It is up to the
/// consumer to ensure that the maps written are in a form that can be used
/// by other tools, e.g. qbsp.
fn check_writable(&self) -> ValidationResult;
}

Expand All @@ -51,22 +59,30 @@ impl error::Error for WriteError {}
#[cfg(feature = "std")]
pub type WriteAttempt = Result<(), WriteError>;

/// 3-dimensional point used to determine the half-space a surface lies on
pub type Point = [f64; 3];
pub type Vec3 = [f64; 3];
pub type Vec2 = [f64; 2];

/// Transparent data structure representing a Quake source map
///
/// Contains a list of entities. Internal texture alignments may be in the
/// original "legacy" Id format, the "Valve 220" format, or a mix of the two.
#[derive(Clone)]
pub struct QuakeMap {
pub entities: Vec<Entity>,
}

impl QuakeMap {
/// Instantiate a new map with 0 entities
pub const fn new() -> Self {
QuakeMap {
entities: Vec::new(),
}
}

/// Writes the map to the provided writer in text format, failing if
/// validation fails or an I/O error occurs
#[cfg(feature = "std")]
pub fn write_to<W: io::Write>(&self, writer: &mut W) -> WriteAttempt {
for ent in &self.entities {
Expand All @@ -86,26 +102,31 @@ impl CheckWritable for QuakeMap {
}
}

/// Entity type: `Brush` for entities with brushes, `Point` for entities without
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EntityKind {
Point,
Brush,
}

/// A collection of key/value pairs in the form of an *edict* and 0 or more
/// brushes
#[derive(Clone)]
pub struct Entity {
pub edict: Edict,
pub brushes: Vec<Brush>,
}

impl Entity {
/// Instantiate a new entity without any keyvalues or brushes
pub fn new() -> Self {
Entity {
edict: Edict::new(),
brushes: Vec::new(),
}
}

/// Determine whether this is a point or brush entity
pub fn kind(&self) -> EntityKind {
if self.brushes.is_empty() {
EntityKind::Point
Expand All @@ -132,6 +153,7 @@ impl Entity {
}

impl Default for Entity {
/// Same as `Entity::new`
fn default() -> Self {
Entity::new()
}
Expand All @@ -149,6 +171,7 @@ impl CheckWritable for Entity {
}
}

/// Entity dictionary
pub type Edict = HashMap<CString, CString>;

impl CheckWritable for Edict {
Expand All @@ -162,6 +185,7 @@ impl CheckWritable for Edict {
}
}

/// Convex polyhedron
pub type Brush = Vec<Surface>;

impl CheckWritable for Brush {
Expand All @@ -174,6 +198,7 @@ impl CheckWritable for Brush {
}
}

/// Brush face
#[derive(Clone)]
pub struct Surface {
pub half_space: HalfSpace,
Expand Down Expand Up @@ -201,6 +226,8 @@ impl CheckWritable for Surface {
}
}

/// A set of 3 points that determine a plane with its facing direction
/// determined by the winding order of the points
pub type HalfSpace = [Point; 3];

impl CheckWritable for HalfSpace {
Expand All @@ -213,11 +240,17 @@ impl CheckWritable for HalfSpace {
}
}

/// Texture alignment properties
///
/// If axes are present, the alignment will be written in the "Valve220" format;
/// otherwise it will be written in the "legacy" format pre-dating Valve220.
#[derive(Clone, Copy)]
pub struct Alignment {
pub offset: Vec2,
pub rotation: f64,
pub scale: Vec2,

/// Describes the X and Y texture-space axes
pub axes: Option<[Vec3; 2]>,
}

Expand Down

0 comments on commit 013ef62

Please sign in to comment.