1#![doc = include_str!("../readme.md")]
2
3extern 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
29pub use glam;
31pub use image;
32pub use qbsp_macros::{BspValue, BspVariableValue};
33pub use smallvec;
34
35pub 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
54pub static QUAKE_PALETTE: Palette = unsafe { mem::transmute_copy(include_bytes!("../palette.lmp")) };
56
57pub struct BspParseInput<'a> {
58 pub bsp: &'a [u8],
60 pub lit: Option<&'a [u8]>,
62
63 pub settings: BspParseSettings,
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct BspParseSettings {
68 pub use_bspx_rgb_lighting: bool,
75 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 #[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 #[error("{0} - {1}")]
115 DoingJob(String, Box<BspParseError>),
116}
117impl BspParseError {
118 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 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#[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 #[default]
170 BSP2,
171
172 BSP29,
174
175 BSP30,
178
179 BSP38,
181
182 BSP38Qbism,
184}
185impl BspFormat {
186 pub fn liquid_prefix(self) -> Option<char> {
188 match self {
189 Self::BSP2 | Self::BSP29 => Some('*'),
191 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 let version: u32 = reader.read()?;
221 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
264pub 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
285pub 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#[derive(Debug, Clone, Default)]
304#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
305#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
306pub struct BspData {
307 pub entities: Vec<u8>,
312 pub planes: Vec<BspPlane>,
313 pub textures: Vec<Option<BspMipTexture>>,
314 pub vertices: Vec<Vec3>,
316 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 pub leaf_brushes: Vec<UBspValue>,
335 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 pub bspx: BspxData,
347
348 pub parse_ctx: BspParseContext,
350}
351
352impl BspData {
353 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 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(), 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 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}