From f8112047f8f154666d224f8388716d5807ce5596 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 15:59:44 -0400 Subject: [PATCH 1/7] Link to documentation --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8eb320f..a2b0f42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "quake-util" -version = "0.1.1" +version = "0.2.0" authors = ["Seth "] 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"] From 016161cb6623b9dfdb0a53abf57d62d44bc1b4c2 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 16:00:46 -0400 Subject: [PATCH 2/7] Make modules with re-exported items private --- src/qmap/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qmap/mod.rs b/src/qmap/mod.rs index db46f3d..b5b7e94 100644 --- a/src/qmap/mod.rs +++ b/src/qmap/mod.rs @@ -1,13 +1,13 @@ -pub mod repr; +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, From c7e0736d5af870839afdc7c07db0116ccca6a41c Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 17:20:47 -0400 Subject: [PATCH 3/7] Initial documentation pass --- src/qmap/mod.rs | 2 ++ src/qmap/parser.rs | 6 ++++++ src/qmap/repr.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/qmap/mod.rs b/src/qmap/mod.rs index b5b7e94..6282cda 100644 --- a/src/qmap/mod.rs +++ b/src/qmap/mod.rs @@ -1,3 +1,5 @@ +//! Quake source map data structures, parsing, and writing + mod repr; #[cfg(feature = "std")] diff --git a/src/qmap/parser.rs b/src/qmap/parser.rs index 2272d57..c151dcc 100644 --- a/src/qmap/parser.rs +++ b/src/qmap/parser.rs @@ -11,6 +11,12 @@ use qmap::ParseResult; type TokenPeekable = Peekable>; 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(reader: R) -> ParseResult { let mut entities: Vec = Vec::new(); let mut peekable_tokens = TokenIterator::new(reader).peekable(); diff --git a/src/qmap/repr.rs b/src/qmap/repr.rs index 208d742..80e5d41 100644 --- a/src/qmap/repr.rs +++ b/src/qmap/repr.rs @@ -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; } @@ -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, } 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(&self, writer: &mut W) -> WriteAttempt { for ent in &self.entities { @@ -86,12 +102,15 @@ 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, @@ -99,6 +118,7 @@ pub struct Entity { } impl Entity { + /// Instantiate a new entity without any keyvalues or brushes pub fn new() -> Self { Entity { edict: Edict::new(), @@ -106,6 +126,7 @@ impl Entity { } } + /// Determine whether this is a point or brush entity pub fn kind(&self) -> EntityKind { if self.brushes.is_empty() { EntityKind::Point @@ -132,6 +153,7 @@ impl Entity { } impl Default for Entity { + /// Same as `Entity::new` fn default() -> Self { Entity::new() } @@ -149,6 +171,7 @@ impl CheckWritable for Entity { } } +/// Entity dictionary pub type Edict = HashMap; impl CheckWritable for Edict { @@ -162,6 +185,7 @@ impl CheckWritable for Edict { } } +/// Convex polyhedron pub type Brush = Vec; impl CheckWritable for Brush { @@ -174,6 +198,7 @@ impl CheckWritable for Brush { } } +/// Brush face #[derive(Clone)] pub struct Surface { pub half_space: HalfSpace, @@ -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 { @@ -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]>, } From 16569233b3d86b120f4aba25d3b8116a49100dcf Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 18:18:05 -0400 Subject: [PATCH 4/7] Add comment example --- src/qmap/mod.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/qmap/mod.rs b/src/qmap/mod.rs index 6282cda..bf58ee9 100644 --- a/src/qmap/mod.rs +++ b/src/qmap/mod.rs @@ -1,4 +1,50 @@ //! 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> { +//! # let source = &b" +//! # { +//! # } +//! # "[..]; +//! # +//! # let mut dest = Vec::::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; From dc71abcd2405e83340011c1ee08b4a8c7b056dde Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 18:27:28 -0400 Subject: [PATCH 5/7] Add .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto From 74cf0afa634263815c9d8148310c6f327767b333 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 19:37:12 -0400 Subject: [PATCH 6/7] More accurate parse errors --- src/qmap/parser.rs | 31 ++++++++++++++++++++++++++++++- src/qmap/parser_test.rs | 19 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/qmap/parser.rs b/src/qmap/parser.rs index c151dcc..7e3ad95 100644 --- a/src/qmap/parser.rs +++ b/src/qmap/parser.rs @@ -89,7 +89,7 @@ fn parse_brush(tokens: &mut TokenPeekable) -> ParseResult { } } - expect_byte(&tokens.next().transpose()?, b'}')?; + expect_byte_or(&tokens.next().transpose()?, b'}', &[b'('])?; Ok(surfaces) } @@ -201,6 +201,35 @@ fn expect_byte(token: &Option, byte: u8) -> ParseResult<()> { } } +fn expect_byte_or( + token: &Option, + 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::>()[..]) + .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) -> ParseResult<()> { match token.as_ref() { Some(payload) if payload.match_quoted() => Ok(()), diff --git a/src/qmap/parser_test.rs b/src/qmap/parser_test.rs index 8352a62..47465c2 100644 --- a/src/qmap/parser_test.rs +++ b/src/qmap/parser_test.rs @@ -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); + } +} From 5233b7f5de57c8b82b78405d31c76765d490fb15 Mon Sep 17 00:00:00 2001 From: seth Date: Sat, 18 Mar 2023 19:49:28 -0400 Subject: [PATCH 7/7] Ignore doctest when default feature disabled --- src/qmap/mod.rs | 58 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/qmap/mod.rs b/src/qmap/mod.rs index bf58ee9..6eb1d9d 100644 --- a/src/qmap/mod.rs +++ b/src/qmap/mod.rs @@ -7,41 +7,45 @@ //! # use std::ffi::CString; //! # use std::io::Read; //! # +//! # //! # fn main() -> Result<(), String> { -//! # let source = &b" -//! # { -//! # } -//! # "[..]; +//! # #[cfg(feature="std")] +//! # { +//! # let source = &b" +//! # { +//! # } +//! # "[..]; //! # -//! # let mut dest = Vec::::new(); +//! # let mut dest = Vec::::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(), -//! })?; +//! use qmap::{Entity, ParseError, WriteError}; //! -//! let mut soldier = Entity::new(); +//! 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(), +//! })?; //! -//! soldier.edict.insert( -//! CString::new("classname").unwrap(), -//! CString::new("monster_army").unwrap(), -//! ); +//! let mut soldier = Entity::new(); //! -//! soldier.edict.insert( -//! CString::new("origin").unwrap(), -//! CString::new("128 -256 24").unwrap(), -//! ); +//! soldier.edict.insert( +//! CString::new("classname").unwrap(), +//! CString::new("monster_army").unwrap(), +//! ); //! -//! map.entities.push(soldier); +//! soldier.edict.insert( +//! CString::new("origin").unwrap(), +//! CString::new("128 -256 24").unwrap(), +//! ); //! -//! map.write_to(&mut dest).map_err(|err| match err { -//! WriteError::Io(e) => e.to_string(), -//! WriteError::Validation(msg) => msg -//! })?; -//! # +//! 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(()) //! # } //! ```