qbsp 0.14.1

Rust crate for parsing and operating with Quake 1, 2, and GoldSrc BSP files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! Data definitions for textures and surface flags.

#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use bitflags::bitflags;
use glam::{Vec2, Vec3, dvec2};
use qbsp_macros::{BspValue, BspVariableValue};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::{
	BspData, BspParseError, BspParseResultDoingJobExt, BspResult, QUAKE_PALETTE,
	data::util::{FixedStr, NoField},
	reader::{BspByteReader, BspParseContext, BspValue, BspVariableValue},
};

/// Stack-allocated string the longest a texture name can be.
pub type TextureName = FixedStr<32>;

/// Stack-allocated string the longest an embedded texture name can be.
pub type EmbeddedTextureName = FixedStr<16>;

impl BspData {
	/// Helper function to get a texture name from a [`BspTexInfo`] without worrying about format.
	pub fn get_texture_name<'a>(&'a self, tex_info: &'a BspTexInfo) -> Option<TextureName> {
		if let Some(extra_info) = &tex_info.extra_info.0 {
			Some(extra_info.name)
		} else if let Some(texture_idx) = tex_info.texture_idx.0 {
			Some(self.textures.get(texture_idx as usize).and_then(Option::as_ref)?.header.name.extend())
		} else {
			None
		}
	}
}

/// An id Tech 2 palette to use for embedded images.
#[repr(C)] // Because we transmute data with QUAKE_PALETTE, don't want Rust to pull any shenanigans
#[derive(Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Palette {
	#[cfg_attr(feature = "serde", serde(with = "serde_big_array::BigArray"))]
	pub colors: [[u8; 3]; 256],
}

impl BspValue for Palette {
	fn bsp_parse(reader: &mut BspByteReader) -> BspResult<Self> {
		let num_colors = reader.read::<i16>()?;

		if num_colors != 256 {
			return Err(BspParseError::InvalidPaletteLength(num_colors as usize));
		}

		let colors = reader.read_bytes(num_colors as usize * 3)?;

		Palette::parse(colors)
	}

	fn bsp_struct_size(ctx: &BspParseContext) -> usize {
		i16::bsp_struct_size(ctx) + 3 * 256
	}
}

impl Default for Palette {
	fn default() -> Self {
		QUAKE_PALETTE.clone()
	}
}

impl Palette {
	/// Parses a palette from data. Palettes must be 768 bytes in length exactly.
	pub fn parse(data: &[u8]) -> BspResult<Self> {
		let (pixels, rest) = data.as_chunks::<3>();
		if !rest.is_empty() {
			return Err(BspParseError::InvalidPaletteLength(data.len()));
		}
		Ok(Self {
			colors: pixels.try_into().map_err(|_| BspParseError::InvalidPaletteLength(data.len()))?,
		})
	}
}

#[derive(BspVariableValue, Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[bsp29(u32)]
#[bsp2(u32)]
#[bsp30(u32)]
#[bsp38(NoField)]
#[qbism(NoField)]
pub struct TextureIdxField(pub Option<u32>);

#[derive(BspVariableValue, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[bsp29(NoField)]
#[bsp2(NoField)]
#[bsp30(NoField)]
#[bsp38(BspTexQ2Info)]
#[qbism(BspTexQ2Info)]
pub struct Q2InfoField(pub Option<BspTexQ2Info>);

#[derive(BspValue, Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspTexInfo {
	pub projection: PlanarTextureProjection,

	pub texture_idx: TextureIdxField,

	/// GoldSrc and Q2 have surface flags, Q1 and BSP2 have texture flags.
	pub flags: BspTexInfoFlags,

	/// Extra info stored directly on the `TexInfo` - for Quake 2 (which does not have a lump for embedded textures).
	pub extra_info: Q2InfoField,
}

impl BspTexQ2Info {
	pub const UNIT_TEXTURE_BRIGHTNESS: u32 = 255;

	/// The brightness of the texture - if in the range 0..1 this material is diffuse, if it's
	/// more than 1 then this texture is emissive. By far the most-common brightness is 1, and
	/// most maps will display reasonably if clamping this value in the 0..1 range and making
	/// all textures diffuse. If doing HDR rendering, the material should have a diffuse
	/// layer at normal brightness plus an emissive layer with the same texture, with emission
	/// set to `1. - brightness`.
	pub fn brightness(&self) -> f64 {
		self.value as f64 / Self::UNIT_TEXTURE_BRIGHTNESS as f64
	}

	/// The scale to apply to the diffuse layer (always in the range 0..1).
	pub fn diffuse_scale(&self) -> f64 {
		self.brightness().min(1.)
	}

	/// If this texture has an emissive component (brightness > 1), this is the emissive scale
	/// for that texture. If this method returns `Some(scale)`, then `scale` will always be
	/// greater than 0.
	pub fn emissive_scale(&self) -> Option<f64> {
		self.value
			// Subtract `UNIT_TEXTURE_BRIGHTNESS + 1` so we return `None` if this is zero...
			.checked_sub(Self::UNIT_TEXTURE_BRIGHTNESS + 1)
			// ...then add 1 back to get the actual scale.
			.map(|brightness| (brightness + 1) as f64 / Self::UNIT_TEXTURE_BRIGHTNESS as f64)
	}
}

impl From<BspTexFlags> for BspTexInfoFlags {
	fn from(value: BspTexFlags) -> Self {
		Self {
			texture_flags: Some(value),
			surface_flags: BspSurfaceFlags::empty(),
		}
	}
}

impl From<BspSurfaceFlags> for BspTexInfoFlags {
	fn from(value: BspSurfaceFlags) -> Self {
		Self {
			texture_flags: None,
			surface_flags: value,
		}
	}
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspTexInfoFlags {
	/// If this is `None`, then the name should be used to check the texture flags (for GoldSrc and Quake 2)
	pub texture_flags: Option<BspTexFlags>,
	/// For BSP2 and BSP29, this is always zero.
	pub surface_flags: BspSurfaceFlags,
}

impl BspVariableValue for BspTexInfoFlags {
	type Bsp29 = BspTexFlags;
	type Bsp2 = BspTexFlags;
	type Bsp30 = BspSurfaceFlags;
	type Bsp38 = BspSurfaceFlags;
	type Qbism = BspSurfaceFlags;
}

/// Quake 1-style texture flags. Quake 2 ditches this entirely, GoldSrc
/// mostly handles this on a per-brush basis using `rendermode` and
/// related entity keys.
#[derive(BspValue, Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u32)]
pub enum BspTexFlags {
	#[default]
	/// Normal lightmapped surface.
	Normal = 0,
	/// No lighting or 256 subdivision.
	Special = 1,
	/// Texture cannot be found.
	Missing = 2,
}

bitflags! {
	#[derive(Debug, Copy, Clone)]
	#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
	#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
	#[repr(transparent)]
	#[cfg_attr(feature = "bevy_reflect", reflect(opaque))]
	pub struct BspSurfaceFlags: u32 {
		/// Never give falling damage
		const NO_DAMAGE = 0b0000_0000_0000_0000_0001;
		/// Affects game physics
		const SLICK = 0b0000_0000_0000_0000_0010;
		/// Lighting from environment map
		const SKY = 0b0000_0000_0000_0000_0100;
		/// Climbable ladder
		const WARP = 0b0000_0000_0000_0000_1000;
		/// Don't make missile explosions
		const NO_IMPACT = 0b0000_0000_0000_0001_0000;
		/// Don't leave missile marks
		const NO_MARKS = 0b0000_0000_0000_0010_0000;
		/// Make flesh sounds and effects
		const FLESH = 0b0000_0000_0000_0100_0000;
		/// Don't generate a drawsurface at all
		const NO_DRAW = 0b0000_0000_0000_1000_0000;
		/// Make a primary bsp splitter
		const HINT = 0b0000_0000_0001_0000_0000;
		/// Completely ignore, allowing non-closed brushes
		const SKIP = 0b0000_0000_0010_0000_0000;
		/// Surface doesn't need a lightmap
		const NO_LIGHTMAP = 0b0000_0000_0100_0000_0000;
		/// Generate lighting info at vertices
		const POINT_LIGHT = 0b0000_0000_1000_0000_0000;
		/// Clanking footsteps
		const METAL_STEPS = 0b0000_0001_0000_0000_0000;
		/// No footstep sounds
		const NO_STEPS = 0b0000_0010_0000_0000_0000;
		/// Don't collide against curves with this set
		const NON_SOLID = 0b0000_0100_0000_0000_0000;
		/// Act as a light filter during q3map -light
		const LIGHT_FILTER = 0b0000_1000_0000_0000_0000;
		/// Do per-pixel light shadow casting in q3map
		const ALPHA_SHADOW = 0b0001_0000_0000_0000_0000;
		/// Never add dynamic lights
		const NO_DLIGHT = 0b0010_0000_0000_0000_0000;
		/// Unused in Quake 2, but included in case you want to add an extension.
		const UNUSED1 = 0b0100_0000_0000_0000_0000;
		/// Unused in Quake 2, but included in case you want to add an extension.
		const UNUSED2 = 0b1000_0000_0000_0000_0000;
	}
}

impl BspValue for BspSurfaceFlags {
	fn bsp_parse(reader: &mut BspByteReader) -> BspResult<Self> {
		u32::bsp_parse(reader).map(BspSurfaceFlags::from_bits_truncate)
	}

	fn bsp_struct_size(ctx: &BspParseContext) -> usize {
		u32::bsp_struct_size(ctx)
	}
}

/// Extra info stored directly on the [`BspTexInfo`] - for Quake 2 (which does not have a lump for embedded textures).
#[derive(BspValue, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspTexQ2Info {
	/// Texture brightness - 255 is "normal" brightness (should display the same as Quake 1),
	/// higher is emissive, lower is darker. To get this as a floating-point number, see
	/// [`BspTexQ2Info::brightness`].
	pub value: u32,

	/// The name of the texture.
	pub name: FixedStr<32>,

	/// For animated textures.
	pub next: i32,
}

/// Texture projection information.
#[derive(BspValue, Debug, Clone, Copy)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PlanarTextureProjection {
	pub u_axis: Vec3,
	pub u_offset: f32,

	pub v_axis: Vec3,
	pub v_offset: f32,
}

impl PlanarTextureProjection {
	/// Projects a position onto this plane.
	///
	/// Converts to double for calculation to minimise floating-point imprecision as demonstrated [here](https://github.com/Novum/vkQuake/blob/b6eb0cf5812c09c661d51e3b95fc08d88da2288a/Quake/gl_model.c#L1315).
	pub fn project(&self, point: Vec3) -> Vec2 {
		dvec2(
			point.as_dvec3().dot(self.u_axis.as_dvec3()) + self.u_offset as f64,
			point.as_dvec3().dot(self.v_axis.as_dvec3()) + self.v_offset as f64,
		)
		.as_vec2()
	}
}

/// GoldSrc (BSP30) levels can have per-texture pallettes stored in WAD files with the WAD3 format.
#[derive(BspVariableValue, Default, Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[bsp29(NoField)]
#[bsp2(NoField)]
#[bsp30(Palette)]
#[bsp38(NoField)]
#[qbism(NoField)]
pub struct Wad3Palette(pub Option<Palette>);

#[derive(Default, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspTextureData {
	pub full: Option<Vec<u8>>,
	pub half: Option<Vec<u8>>,
	pub quarter: Option<Vec<u8>>,
	pub eighth: Option<Vec<u8>>,
	pub palette: Wad3Palette,
}

impl std::fmt::Debug for BspTextureData {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.debug_struct("BspTextureData")
			.field("full", &self.full.as_ref().map(|_| ..))
			.field("half", &self.half.as_ref().map(|_| ..))
			.field("quarter", &self.quarter.as_ref().map(|_| ..))
			.field("eighth", &self.eighth.as_ref().map(|_| ..))
			.finish()
	}
}

/// Embedded texture data. GoldSrc stores these in a separate lump.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspMipTexture {
	pub header: BspTextureHeader,
	pub data: BspTextureData,
}

impl BspValue for BspMipTexture {
	fn bsp_parse(reader: &mut BspByteReader) -> BspResult<Self> {
		let reader_start = reader.pos();

		let header: BspTextureHeader = reader.read()?;

		if header.width == 0 || header.height == 0 {
			return Ok(Self {
				header,
				data: BspTextureData::default(),
			});
		}

		macro_rules! read_data {
			($offset:ident, $res:literal $(, $($res_operator:tt)+)?) => {{
				if header.$offset == 0 {
					None
				} else {
					// The offsets are relative to start of the header, so reset state before continuing.
					*reader = reader.with_pos(reader_start + header.$offset as usize);

					Some(
						reader
							.read_bytes((header.width as usize $($($res_operator)+)?) * (header.height as usize $($($res_operator)+)?))
							.job(|| format!(concat!("Reading texture (", $res, "res) with header {:#?}"), header))?
							.to_vec(),
					)
				}
			}};
		}

		if [header.offset_full, header.offset_half, header.offset_quarter, header.offset_eighth]
			.into_iter()
			.all(|o| o == 0)
		{
			Ok(Self {
				data: Default::default(),
				header,
			})
		} else {
			Ok(Self {
				data: BspTextureData {
					full: read_data!(offset_full, "full"),
					half: read_data!(offset_half, "half", / 2),
					quarter: read_data!(offset_quarter, "quarter", / 4),
					eighth: read_data!(offset_eighth, "eighth", / 8),
					// We do not reset state after the last read, as the next should start directly after.
					palette: reader.read()?,
				},
				header,
			})
		}
	}

	fn bsp_struct_size(ctx: &BspParseContext) -> usize {
		BspTextureHeader::bsp_struct_size(ctx)
	}
}

#[derive(BspValue, Debug, Clone)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BspTextureHeader {
	pub name: EmbeddedTextureName,

	pub width: u32,
	pub height: u32,

	pub offset_full: u32,
	#[allow(unused)]
	pub offset_half: u32,
	#[allow(unused)]
	pub offset_quarter: u32,
	#[allow(unused)]
	pub offset_eighth: u32,
}