Skip to main content

qbsp/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3// For proc macros to be able to use the `qbsp` path.
4extern crate self as qbsp;
5
6#[cfg(feature = "bevy_reflect")]
7use bevy_reflect::Reflect;
8use glam::Vec3;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13pub mod prelude;
14use std::mem;
15
16pub mod data;
17
18#[cfg(feature = "meshing")]
19pub mod mesh;
20
21#[cfg(test)]
22pub mod loading_tests;
23pub mod query;
24pub mod reader;
25pub mod util;
26
27pub use data::bspx;
28
29// Re-exports
30pub use glam;
31pub use image;
32pub use qbsp_macros::{BspValue, BspVariableValue};
33pub use smallvec;
34
35// Re-export since this will be one of the most-used types when configuring `qbsp`.
36pub use data::texture::Palette;
37
38use crate::{
39	data::{
40		LumpDirectory, LumpEntry,
41		brush::{BspBrush, BspBrushSide},
42		bspx::BspxData,
43		lighting::{BspLighting, read_lit},
44		models::{BspEdge, BspFace, BspModel},
45		nodes::{BspClipNode, BspLeaf, BspNode, BspPlane},
46		texture::{BspMipTexture, BspTexInfo},
47		util::UBspValue,
48		visdata::BspVisData,
49	},
50	reader::{BspByteReader, BspParseContext, BspValue},
51	util::display_magic_number,
52};
53
54/// The default quake palette.
55pub static QUAKE_PALETTE: Palette = unsafe { mem::transmute_copy(include_bytes!("../palette.lmp")) };
56
57pub struct BspParseInput<'a> {
58	/// The data for the BSP file itself.
59	pub bsp: &'a [u8],
60	/// The optional .lit file for external colored lighting.
61	pub lit: Option<&'a [u8]>,
62
63	pub settings: BspParseSettings,
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct BspParseSettings {
68	/// If `true`, will use the `RGBLIGHTING` BSPX lump if it exists to supply [`BspData::lighting`].
69	/// This will not work if [`parse_bspx_structures`](Self::parse_bspx_structures) if `false`.
70	///
71	/// NOTE: This moves out of [`BspxData::rgb_lighting`], and is a lossy operation. This should only be used for reading.
72	///
73	/// (Default: `true`)
74	pub use_bspx_rgb_lighting: bool,
75	/// Automatically parses BSPX structures into fields in [`BspxData`]. If `false`, all BSPX lumps will be in [`BspxData::unparsed`]. (Default: `true`)
76	pub parse_bspx_structures: bool,
77}
78impl Default for BspParseSettings {
79	fn default() -> Self {
80		Self {
81			use_bspx_rgb_lighting: true,
82			parse_bspx_structures: true,
83		}
84	}
85}
86
87#[derive(Debug, Clone, Error)]
88pub enum BspParseError {
89	#[error("Palette byte length {0} instead of 768.")]
90	InvalidPaletteLength(usize),
91	#[error("Lump ({0:?}) out of bounds of data! Malformed/corrupted BSP?")]
92	LumpOutOfBounds(LumpEntry),
93	#[error("Tried to read bytes from {from} to {to} from buffer of size {size}")]
94	BufferOutOfBounds { from: usize, to: usize, size: usize },
95	#[error("Failed to parse string at index {index}, invalid utf-8 sequence: {sequence:?}")]
96	InvalidString { index: usize, sequence: Vec<u8> },
97	#[error("Wrong magic number! Expected {expected}, found \"{}\"", display_magic_number(found))]
98	WrongMagicNumber { found: [u8; 4], expected: &'static str },
99	#[error("Unsupported BSP version! Expected {expected}, found {found}")]
100	UnsupportedBspVersion { found: u32, expected: &'static str },
101	#[error("Invalid color data, size {0} is not devisable by 3!")]
102	ColorDataSizeNotDevisableBy3(usize),
103	#[error("Invalid value: {value}, acceptable:\n{acceptable}")]
104	InvalidVariant { value: i32, acceptable: &'static str },
105	/// This is to be gracefully handled in-crate.
106	#[error("No BSPX directory")]
107	NoBspxDirectory,
108	#[error("No BSPX lump: {0}")]
109	NoBspxLump(String),
110	#[error("Duplicate BSPX lump: {0}")]
111	DuplicateBspxLump(String),
112
113	/// For telling the user exactly where the error occurred in the process.
114	#[error("{0} - {1}")]
115	DoingJob(String, Box<BspParseError>),
116}
117impl BspParseError {
118	/// The error error behind any [`BspParseError::DoingJob`].
119	pub fn root(&self) -> &BspParseError {
120		let mut err = self;
121		loop {
122			match err {
123				Self::DoingJob(_, child) => err = child,
124				_ => return err,
125			}
126		}
127	}
128
129	#[inline]
130	pub fn map_utf8_error(data: &[u8]) -> impl FnOnce(std::str::Utf8Error) -> Self + '_ {
131		|err| BspParseError::InvalidString {
132			index: err.valid_up_to(),
133			sequence: data[err.valid_up_to()..err.valid_up_to() + err.error_len().unwrap_or(1)].to_vec(),
134		}
135	}
136}
137
138pub type BspResult<T> = Result<T, BspParseError>;
139
140pub trait BspParseResultDoingJobExt<T> {
141	/// Like `map_err`, but specifically for adding messages to BSP errors to tell the user exactly what was going on when the error occurred.
142	fn job(self, job: T) -> Self;
143}
144impl<T> BspParseResultDoingJobExt<&str> for BspResult<T> {
145	#[inline]
146	fn job(self, job: &str) -> Self {
147		match self {
148			Ok(v) => Ok(v),
149			Err(err) => Err(BspParseError::DoingJob(job.to_owned(), Box::new(err))),
150		}
151	}
152}
153impl<T, F: FnOnce() -> String> BspParseResultDoingJobExt<F> for BspResult<T> {
154	#[inline]
155	fn job(self, job: F) -> Self {
156		match self {
157			Ok(v) => Ok(v),
158			Err(err) => Err(BspParseError::DoingJob((job)(), Box::new(err))),
159		}
160	}
161}
162
163/// The format of a BSP file. This is determined by the magic number made up of the first 4 bytes of the file, and governs how the rest of the file attempts to parse.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
165#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
166#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
167pub enum BspFormat {
168	/// Modern BSP format with expanded limits
169	#[default]
170	BSP2,
171
172	/// Original quake format, in most cases, you should use BSP2 over this.
173	BSP29,
174
175	/// GoldSrc format. For the sake of `BspVariableValue`, this is usually the same as `BSP38`,
176	/// but differs in some cases (e.g. each model having up to 4 hulls).
177	BSP30,
178
179	/// Quake 2 format.
180	BSP38,
181
182	/// Quake 2 format with expanded limits, similar to what BSP2 is to BSP29.
183	BSP38Qbism,
184}
185impl BspFormat {
186	/// Returns the character used to denote liquids by prefixing the texture name in the engine that uses this format.
187	pub fn liquid_prefix(self) -> Option<char> {
188		match self {
189			// https://quakewiki.org/wiki/Textures
190			Self::BSP2 | Self::BSP29 => Some('*'),
191			// https://developer.valvesoftware.com/wiki/Texture_prefixes
192			Self::BSP30 => Some('!'),
193			Self::BSP38 | Self::BSP38Qbism => None,
194		}
195	}
196
197	pub const fn is_quake1(self) -> bool {
198		matches!(self, Self::BSP29 | Self::BSP2)
199	}
200
201	pub const fn is_goldsrc(self) -> bool {
202		matches!(self, Self::BSP30)
203	}
204
205	pub const fn is_quake2(self) -> bool {
206		matches!(self, Self::BSP38 | Self::BSP38Qbism)
207	}
208}
209
210impl BspValue for BspFormat {
211	fn bsp_parse(reader: &mut BspByteReader) -> BspResult<Self> {
212		let magic_number: [u8; 4] = reader.read()?;
213
214		match &magic_number {
215			b"BSP2" => Ok(Self::BSP2),
216			[0x1D, 0x00, 0x00, 0x00] => Ok(Self::BSP29),
217			[0x1E, 0x00, 0x00, 0x00] => Ok(Self::BSP30),
218			b"IBSP" => {
219				// "IBSP" is shared among formats, like Quake 3. Instead, it is differentiated by a version number read after the magic number.
220				let version: u32 = reader.read()?;
221				// Currently, we only support version 38, the Quake2 format.
222				match version {
223					38 => Ok(Self::BSP38),
224					_ => Err(BspParseError::UnsupportedBspVersion {
225						found: version,
226						expected: "38 (Quake 2)",
227					}),
228				}
229			}
230			b"QBSP" => {
231				let version: u32 = reader.read()?;
232				match version {
233					38 => Ok(Self::BSP38Qbism),
234					_ => Err(BspParseError::UnsupportedBspVersion {
235						found: version,
236						expected: "38 (Quake 2)",
237					}),
238				}
239			}
240			_ => Err(BspParseError::WrongMagicNumber {
241				found: magic_number,
242				expected: "BSP2, 0x1D000000 (BSP29), 0x1E000000 (BSP30), IBSP (BSP38), or QBSP (QBISM)",
243			}),
244		}
245	}
246
247	fn bsp_struct_size(_ctx: &BspParseContext) -> usize {
248		unimplemented!("BspFormat can be of 4 or 8 bytes depending on whether it needs to read version number.");
249	}
250}
251
252impl std::fmt::Display for BspFormat {
253	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254		match self {
255			BspFormat::BSP2 => write!(f, "BSP2"),
256			BspFormat::BSP29 => write!(f, "BSP29"),
257			BspFormat::BSP30 => write!(f, "BSP30"),
258			BspFormat::BSP38 => write!(f, "BSP38"),
259			BspFormat::BSP38Qbism => write!(f, "BSP38 (Qbism)"),
260		}
261	}
262}
263
264/// Helper function to read an array of data of type `T` from a lump. Takes in the BSP file data, the lump directory, and the lump to read from.
265pub fn read_lump<T: BspValue>(data: &[u8], entry: LumpEntry, lump_name: &'static str, ctx: &BspParseContext) -> BspResult<Vec<T>> {
266	let lump_data = entry.get(data)?;
267	assert_eq!(
268		entry.len as usize % T::bsp_struct_size(ctx),
269		0,
270		"Lump {lump_name} is the wrong size for {}",
271		std::any::type_name::<T>()
272	);
273	let lump_entries = entry.len as usize / T::bsp_struct_size(ctx);
274
275	let mut reader = BspByteReader::new(lump_data, ctx);
276	let mut out = Vec::with_capacity(lump_entries);
277
278	for i in 0..lump_entries {
279		out.push(reader.read().job(|| format!("Parsing {lump_name} lump entry {i}"))?);
280	}
281
282	Ok(out)
283}
284
285/// The texture lump is more complex than just a vector of the same type of item, so it needs its own function.
286pub fn read_mip_texture_lump(reader: &mut BspByteReader) -> BspResult<Vec<Option<BspMipTexture>>> {
287	let mut textures = Vec::new();
288	let num_mip_textures: u32 = reader.read()?;
289
290	for _ in 0..num_mip_textures {
291		let offset: i32 = reader.read()?;
292		if offset.is_negative() {
293			textures.push(None);
294			continue;
295		}
296		textures.push(Some(BspMipTexture::bsp_parse(&mut reader.with_pos(offset as usize))?));
297	}
298
299	Ok(textures)
300}
301
302/// A BSP files contents parsed into structures for easy access.
303#[derive(Debug, Clone, Default)]
304#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
305#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
306pub struct BspData {
307	/// Essentially an embedded .map file, the differences being:
308	/// - Brush data has been stripped.
309	/// - Brush entities have a `model` property indexing into the `models` field of this struct.
310	/// - Non UTF-8 text format. Use [`quake_string_to_utf8(...)`](util::quake_string_to_utf8) and [`quake_string_to_utf8_lossy(...)`](util::quake_string_to_utf8_lossy) to convert.
311	pub entities: Vec<u8>,
312	pub planes: Vec<BspPlane>,
313	pub textures: Vec<Option<BspMipTexture>>,
314	/// All vertex positions.
315	pub vertices: Vec<Vec3>,
316	/// RLE encoded bit array. For BSP38, this is cluster-based. For BSP29, BSP2 and BSP30 this is leaf-based,
317	/// and models will have their own indices into the visdata array.
318	///
319	/// Use [`potentially_visible_set_at()`](Self::potentially_visible_set_at) and related functions to query this data.
320	///
321	/// See [the specification](https://www.gamers.org/dEngine/quake/spec/quake-spec34/qkspec_4.htm#BL4) for more info.
322	pub visibility: BspVisData,
323	pub nodes: Vec<BspNode>,
324	pub tex_info: Vec<BspTexInfo>,
325	pub faces: Vec<BspFace>,
326	pub lighting: Option<BspLighting>,
327	pub clip_nodes: Vec<BspClipNode>,
328	pub leaves: Vec<BspLeaf>,
329	/// Used for collision in Quake 2 (BSP38) maps as they don't use hulls.
330	/// Indexes into the [`brushes`](Self::brushes) vector.
331	/// Index into this vector via [`BspLeaf::leaf_brushes`].
332	///
333	/// If this isn't a Quake 2 map, this vector should be empty.
334	pub leaf_brushes: Vec<UBspValue>,
335	/// Indices into the face list, pointed to by leaves.
336	pub mark_surfaces: Vec<UBspValue>,
337	pub edges: Vec<BspEdge>,
338	pub surface_edges: Vec<i32>,
339	pub models: Vec<BspModel>,
340	pub brushes: Vec<BspBrush>,
341	pub brush_sides: Vec<BspBrushSide>,
342	// TODO: Areas/area portals are used by Q2 to stop rendering areas after
343	// doors close - useful but not required to behave correctly.
344	// pub areas: (),
345	// pub area_portals: (),
346	pub bspx: BspxData,
347
348	/// Additional information from the BSP parsed. For example, contains the [BspFormat] of the file.
349	pub parse_ctx: BspParseContext,
350}
351
352impl BspData {
353	/// Parses the data from BSP input.
354	pub fn parse(input: BspParseInput) -> BspResult<Self> {
355		let BspParseInput { bsp, lit, settings } = input;
356		if bsp.len() < 4 {
357			return Err(BspParseError::BufferOutOfBounds {
358				from: 0,
359				to: 4,
360				size: bsp.len(),
361			});
362		}
363
364		// To parse the format version and form the BspParseContext, we need one with a default parse context where it won't be used.
365		let dummy_ctx = BspParseContext::default();
366		let mut reader = BspByteReader::new(bsp, &dummy_ctx);
367
368		let ctx = BspParseContext { format: reader.read()? };
369		let mut reader = reader.with_context(&ctx);
370
371		let lump_dir: LumpDirectory = reader.read()?;
372
373		let mut data = Self {
374			entities: lump_dir.entities.get(bsp)?.to_vec(),
375			planes: read_lump(bsp, lump_dir.planes, "planes", &ctx)?,
376			textures: if let Some(tex_lump) = *lump_dir.textures {
377				read_mip_texture_lump(&mut BspByteReader::new(tex_lump.get(bsp)?, &ctx)).job("Reading texture lump")?
378			} else {
379				Vec::new()
380			},
381			vertices: read_lump(bsp, lump_dir.vertices, "vertices", &ctx).job("vertices")?,
382			visibility: BspByteReader::new(lump_dir.visibility.get(bsp)?, &ctx).read().job("visibility")?,
383			nodes: read_lump(bsp, lump_dir.nodes, "nodes", &ctx)?,
384			tex_info: read_lump(bsp, lump_dir.tex_info, "texture infos", &ctx)?,
385			faces: read_lump(bsp, lump_dir.faces, "faces", &ctx)?,
386			lighting: if let Some(lit) = lit {
387				Some(BspLighting::Colored(read_lit(lit, &ctx, false).job("Parsing .lit file")?))
388			} else if !lump_dir.lighting.is_empty() {
389				Some(BspByteReader::new(lump_dir.lighting.get(bsp)?, &ctx).read()?)
390			} else {
391				None
392			},
393			clip_nodes: if let Some(clip_node_lump) = *lump_dir.clip_nodes {
394				read_lump(bsp, clip_node_lump, "clip nodes", &ctx)?
395			} else {
396				Vec::new()
397			},
398			leaves: read_lump(bsp, lump_dir.leaves, "leaves", &ctx)?,
399			leaf_brushes: if let Some(lump_entry) = *lump_dir.leaf_brushes {
400				read_lump(bsp, lump_entry, "leaf brushes", &ctx)?
401			} else {
402				Vec::new()
403			},
404			mark_surfaces: read_lump(bsp, lump_dir.mark_surfaces, "mark surfaces", &ctx)?,
405			edges: read_lump(bsp, lump_dir.edges, "edges", &ctx)?,
406			surface_edges: read_lump(bsp, lump_dir.surf_edges, "surface edges", &ctx)?,
407			models: read_lump(bsp, lump_dir.models, "models", &ctx)?,
408			brushes: if let Some(lump_entry) = *lump_dir.brushes {
409				read_lump(bsp, lump_entry, "brushes", &ctx)?
410			} else {
411				Vec::new()
412			},
413			brush_sides: if let Some(lump_entry) = *lump_dir.brush_sides {
414				read_lump(bsp, lump_entry, "brush sides", &ctx)?
415			} else {
416				Vec::new()
417			},
418
419			bspx: BspxData::default(), // To be set in a moment.
420
421			parse_ctx: ctx,
422		};
423
424		if let Some(bspx_dir) = &lump_dir.bspx {
425			let mut bspx = BspxData::parse(bsp, bspx_dir, &data).job("Reading BSPX data")?;
426
427			if settings.use_bspx_rgb_lighting
428				&& let Some(lighting) = mem::take(&mut bspx.rgb_lighting)
429			{
430				data.lighting = Some(BspLighting::Colored(lighting));
431			}
432
433			data.bspx = bspx;
434		}
435
436		Ok(data)
437	}
438
439	/// Parses embedded textures using the provided palette. Use [`QUAKE_PALETTE`] for the default Quake palette.
440	pub fn parse_embedded_textures<'a, 'p: 'a>(&'a self, palette: &'p Palette) -> impl Iterator<Item = (&'a str, image::RgbImage)> + 'a {
441		self.textures.iter().flatten().filter_map(|texture| {
442			let Some(data) = &texture.data.full else {
443				return None;
444			};
445
446			let image = image::RgbImage::from_fn(texture.header.width, texture.header.height, |x, y| {
447				image::Rgb(palette.colors[data[(y * texture.header.width + x) as usize] as usize])
448			});
449
450			Some((texture.header.name.as_str(), image))
451		})
452	}
453}