#![allow(clippy::indexing_slicing)]
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use crate::wad::Wad;
pub struct TextureData {
pub palettes: Vec<Palette>,
pub colormaps: Vec<ColorMap>,
pub flats: Vec<FlatData>,
pub textures: Vec<WallTexture>,
pub first_flat: usize,
pub num_flats: usize,
pub flat_names: Vec<[u8; 8]>,
pub texture_names: Vec<[u8; 8]>,
pub sprites: Vec<SpriteDef>,
pub sprite_info: Vec<SpriteInfo>,
pub first_sprite_lump: usize,
}
#[derive(Clone, Default)]
pub struct SpriteDef {
pub frames: Vec<SpriteFrame>,
}
#[derive(Clone)]
pub struct SpriteFrame {
pub rotate: bool,
pub lump: [i16; 8],
pub flip: [bool; 8],
}
impl Default for SpriteFrame {
fn default() -> Self {
Self {
rotate: false,
lump: [-1; 8],
flip: [false; 8],
}
}
}
#[derive(Clone, Default)]
pub struct SpriteInfo {
pub width: i32, pub height: i32,
pub left_offset: i32, pub top_offset: i32, pub patch: Option<Patch>,
}
pub type Palette = [u8; 768]; pub type ColorMap = [u8; 256];
pub struct FlatData {
pub pixels: [u8; 4096],
}
pub struct WallTexture {
pub width: u16,
pub height: u16,
pub data: Vec<u8>,
}
impl WallTexture {
#[inline]
pub fn column(&self, col: usize) -> &[u8] {
let h = self.height as usize;
let start = col * h;
&self.data[start..start + h]
}
}
#[derive(Clone, Copy)]
pub struct Post {
pub top_delta: u8,
pub pixel_offset: u32,
pub pixel_len: u16,
}
impl Post {
#[inline]
pub fn pixels<'a>(&self, pixel_data: &'a [u8]) -> &'a [u8] {
&pixel_data[self.pixel_offset as usize..self.pixel_offset as usize + self.pixel_len as usize]
}
}
#[derive(Clone)]
pub struct Patch {
pub width: u16,
pub height: u16,
pub left_offset: i16,
pub top_offset: i16,
pub columns: Vec<Vec<Post>>,
pub pixel_data: Vec<u8>,
}
#[derive(Debug)]
pub enum TextureError {
MissingLump(String),
InvalidData(String),
}
impl fmt::Display for TextureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingLump(name) => write!(f, "missing lump: {name}"),
Self::InvalidData(msg) => write!(f, "invalid texture data: {msg}"),
}
}
}
impl core::error::Error for TextureError {}
fn r16(data: &[u8], off: usize) -> i16 {
let Some(slice) = data.get(off..off + 2) else { return 0 };
let mut bytes = [0u8; 2];
bytes.copy_from_slice(slice);
i16::from_le_bytes(bytes)
}
fn ru16(data: &[u8], off: usize) -> u16 {
let Some(slice) = data.get(off..off + 2) else { return 0 };
let mut bytes = [0u8; 2];
bytes.copy_from_slice(slice);
u16::from_le_bytes(bytes)
}
fn r32(data: &[u8], off: usize) -> i32 {
let Some(slice) = data.get(off..off + 4) else { return 0 };
let mut bytes = [0u8; 4];
bytes.copy_from_slice(slice);
i32::from_le_bytes(bytes)
}
fn read_name(data: &[u8], off: usize) -> [u8; 8] {
let mut name = [0u8; 8];
name.copy_from_slice(&data[off..off + 8]);
for b in &mut name {
*b = b.to_ascii_uppercase();
}
name
}
impl Patch {
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < 8 {
return None;
}
let width = ru16(data, 0);
let height = ru16(data, 2);
let left_offset = r16(data, 4);
let top_offset = r16(data, 6);
if width == 0 || width > 4096 || height == 0 || height > 4096 {
return None;
}
let col_ofs_end = 8 + (width as usize) * 4;
if col_ofs_end > data.len() {
return None;
}
let mut columns = Vec::with_capacity(width as usize);
let mut pixel_data = Vec::new();
for col in 0..width as usize {
let col_offset = r32(data, 8 + col * 4) as usize;
let posts = parse_column_posts(data, col_offset, &mut pixel_data);
columns.push(posts);
}
Some(Self {
width,
height,
left_offset,
top_offset,
columns,
pixel_data,
})
}
}
fn parse_column_posts(data: &[u8], mut offset: usize, pixel_data: &mut Vec<u8>) -> Vec<Post> {
let mut posts = Vec::new();
loop {
if offset >= data.len() {
break;
}
let top_delta = data[offset];
if top_delta == 0xFF {
break; }
if offset + 1 >= data.len() {
break;
}
let length = data[offset + 1] as usize;
let pixel_start = offset + 3; let pixel_end = pixel_start + length;
if pixel_end > data.len() {
break;
}
let pix_offset = pixel_data.len() as u32;
pixel_data.extend_from_slice(&data[pixel_start..pixel_end]);
posts.push(Post {
top_delta,
pixel_offset: pix_offset,
pixel_len: length as u16,
});
offset = pixel_end + 1; }
posts
}
impl TextureData {
pub fn load(wad: &Wad) -> Result<Self, TextureError> {
let palettes = Self::load_palettes(wad)?;
let colormaps = Self::load_colormaps(wad)?;
let (flats, first_flat, num_flats, flat_names) = Self::load_flats(wad)?;
let (textures, texture_names) = Self::load_textures(wad)?;
let (sprites, sprite_info, first_sprite_lump) = Self::load_sprites(wad)?;
Ok(Self {
palettes,
colormaps,
flats,
textures,
first_flat,
num_flats,
flat_names,
texture_names,
sprites,
sprite_info,
first_sprite_lump,
})
}
fn load_palettes(wad: &Wad) -> Result<Vec<Palette>, TextureError> {
let data = wad
.lump_by_name("PLAYPAL")
.ok_or_else(|| TextureError::MissingLump("PLAYPAL".into()))?;
if data.len() < 768 {
return Err(TextureError::InvalidData("PLAYPAL too small".into()));
}
let num_palettes = data.len() / 768;
let mut palettes = Vec::with_capacity(num_palettes);
for i in 0..num_palettes {
let mut pal = [0u8; 768];
pal.copy_from_slice(&data[i * 768..(i + 1) * 768]);
palettes.push(pal);
}
Ok(palettes)
}
fn load_colormaps(wad: &Wad) -> Result<Vec<ColorMap>, TextureError> {
let data = wad
.lump_by_name("COLORMAP")
.ok_or_else(|| TextureError::MissingLump("COLORMAP".into()))?;
let num_maps = data.len() / 256;
let mut colormaps = Vec::with_capacity(num_maps);
for i in 0..num_maps {
let mut cmap = [0u8; 256];
cmap.copy_from_slice(&data[i * 256..(i + 1) * 256]);
colormaps.push(cmap);
}
Ok(colormaps)
}
#[allow(clippy::type_complexity)]
fn load_flats(wad: &Wad) -> Result<(Vec<FlatData>, usize, usize, Vec<[u8; 8]>), TextureError> {
let f_start = wad
.find_lump("F_START")
.ok_or_else(|| TextureError::MissingLump("F_START".into()))?;
let f_end = wad
.find_lump("F_END")
.ok_or_else(|| TextureError::MissingLump("F_END".into()))?;
let first_flat = f_start + 1;
let num_flats = f_end - first_flat;
let mut flats = Vec::with_capacity(num_flats);
let mut flat_names = Vec::with_capacity(num_flats);
for i in 0..num_flats {
let lump_idx = first_flat + i;
let data = wad.lump_data(lump_idx);
flat_names.push(*wad.lump_name(lump_idx));
let mut pixels = [0u8; 4096];
if data.len() >= 4096 {
pixels.copy_from_slice(&data[..4096]);
}
flats.push(FlatData { pixels });
}
Ok((flats, first_flat, num_flats, flat_names))
}
fn load_textures(wad: &Wad) -> Result<(Vec<WallTexture>, Vec<[u8; 8]>), TextureError> {
let pnames_data = wad
.lump_by_name("PNAMES")
.ok_or_else(|| TextureError::MissingLump("PNAMES".into()))?;
let num_pnames = r32(pnames_data, 0) as usize;
let mut patch_lump_indices = Vec::with_capacity(num_pnames);
for i in 0..num_pnames {
let name_off = 4 + i * 8;
let mut name = [0u8; 8];
name.copy_from_slice(&pnames_data[name_off..name_off + 8]);
let mut name_str = [0u8; 8];
for (j, b) in name.iter().enumerate() {
if *b == 0 {
break;
}
name_str[j] = b.to_ascii_uppercase();
}
let lump_idx = find_lump_by_name8(wad, &name_str);
patch_lump_indices.push(lump_idx);
}
let mut all_textures = Vec::new();
let mut all_names = Vec::new();
if let Some(tex1_data) = wad.lump_by_name("TEXTURE1") {
let (mut texs, mut names) =
parse_texture_lump(tex1_data, wad, &patch_lump_indices)?;
all_textures.append(&mut texs);
all_names.append(&mut names);
}
if let Some(tex2_data) = wad.lump_by_name("TEXTURE2") {
let (mut texs, mut names) =
parse_texture_lump(tex2_data, wad, &patch_lump_indices)?;
all_textures.append(&mut texs);
all_names.append(&mut names);
}
Ok((all_textures, all_names))
}
fn load_sprites(
wad: &Wad,
) -> Result<(Vec<SpriteDef>, Vec<SpriteInfo>, usize), TextureError> {
use crate::math::FRACBITS;
let s_start = wad.find_lump("S_START").unwrap_or(0);
let s_end = wad.find_lump("S_END").unwrap_or(0);
let first_sprite_lump = if s_start > 0 { s_start + 1 } else { 0 };
let num_sprite_lumps = s_end.saturating_sub(first_sprite_lump);
let mut sprite_info = Vec::with_capacity(num_sprite_lumps);
for i in 0..num_sprite_lumps {
let data = wad.lump_data(first_sprite_lump + i);
if data.len() >= 8 {
let width = r16(data, 0) as i32;
let height = r16(data, 2) as i32;
let left_offset = r16(data, 4) as i32;
let top_offset = r16(data, 6) as i32;
sprite_info.push(SpriteInfo {
width: width << FRACBITS,
height: height << FRACBITS,
left_offset: left_offset << FRACBITS,
top_offset: top_offset << FRACBITS,
patch: Patch::parse(data),
});
} else {
sprite_info.push(SpriteInfo::default());
}
}
use alloc::collections::BTreeMap;
type SpriteEntry = (u8, u8, usize, bool);
let mut sprite_map: BTreeMap<[u8; 4], Vec<SpriteEntry>> = BTreeMap::new();
for i in 0..num_sprite_lumps {
let name = wad.lump_name(first_sprite_lump + i);
if name[0] == 0 {
continue;
}
let mut prefix = [0u8; 4];
prefix.copy_from_slice(&name[0..4]);
for b in &mut prefix {
*b = b.to_ascii_uppercase();
}
if name[4] != 0 {
let frame = name[4].to_ascii_uppercase().wrapping_sub(b'A');
let rot = if name[5] >= b'0' && name[5] <= b'8' {
name[5] - b'0'
} else {
0
};
sprite_map
.entry(prefix)
.or_default()
.push((frame, rot, i, false));
}
if name[6] != 0 {
let frame = name[6].to_ascii_uppercase().wrapping_sub(b'A');
let rot = if name[7] >= b'0' && name[7] <= b'8' {
name[7] - b'0'
} else {
0
};
sprite_map
.entry(prefix)
.or_default()
.push((frame, rot, i, true)); }
}
let mut sprites = Vec::new();
for spr_name in &crate::game_data::SPRNAMES {
let mut key = [0u8; 4];
for (i, &b) in spr_name.as_bytes().iter().take(4).enumerate() {
key[i] = b.to_ascii_uppercase();
}
let Some(entries) = sprite_map.get(&key) else {
sprites.push(SpriteDef::default());
continue;
};
let max_frame = entries.iter().map(|(f, _, _, _)| *f).max().unwrap_or(0);
let mut frames = vec![SpriteFrame::default(); max_frame as usize + 1];
for &(frame, rot, lump_idx, is_flipped) in entries {
let f = frame as usize;
if f >= frames.len() {
continue;
}
if rot == 0 {
frames[f].rotate = false;
for r in 0..8 {
frames[f].lump[r] = lump_idx as i16;
frames[f].flip[r] = is_flipped;
}
} else {
frames[f].rotate = true;
let r = (rot - 1) as usize;
if r < 8 {
frames[f].lump[r] = lump_idx as i16;
frames[f].flip[r] = is_flipped;
}
}
}
sprites.push(SpriteDef { frames });
}
Ok((sprites, sprite_info, first_sprite_lump))
}
pub fn texture_num_for_name(&self, name: &[u8; 8]) -> i16 {
if name[0] == b'-' || name[0] == 0 {
return 0;
}
let target = {
let mut n = *name;
for b in &mut n {
*b = b.to_ascii_uppercase();
}
n
};
for (i, tname) in self.texture_names.iter().enumerate() {
if names_match(tname, &target) {
return i as i16;
}
}
0 }
pub fn flat_num_for_name(&self, name: &[u8; 8]) -> i16 {
if name[0] == b'-' || name[0] == 0 {
return -1;
}
let target = {
let mut n = *name;
for b in &mut n {
*b = b.to_ascii_uppercase();
}
n
};
for (i, fname) in self.flat_names.iter().enumerate() {
if names_match(fname, &target) {
return i as i16;
}
}
-1
}
}
fn names_match(a: &[u8; 8], b: &[u8; 8]) -> bool {
for i in 0..8 {
let ac = if a[i] == 0 { 0 } else { a[i].to_ascii_uppercase() };
let bc = if b[i] == 0 { 0 } else { b[i].to_ascii_uppercase() };
if ac != bc {
return false;
}
if ac == 0 {
return true;
}
}
true
}
fn find_lump_by_name8(wad: &Wad, name: &[u8; 8]) -> Option<usize> {
let len = name.iter().position(|&b| b == 0).unwrap_or(8);
let s = core::str::from_utf8(&name[..len]).ok()?;
wad.find_lump(s)
}
fn parse_texture_lump(
data: &[u8],
wad: &Wad,
patch_lump_indices: &[Option<usize>],
) -> Result<(Vec<WallTexture>, Vec<[u8; 8]>), TextureError> {
if data.len() < 4 {
return Err(TextureError::InvalidData("texture lump too small".into()));
}
let num_textures = r32(data, 0) as usize;
let mut textures = Vec::with_capacity(num_textures);
let mut names = Vec::with_capacity(num_textures);
for i in 0..num_textures {
let offset_pos = 4 + i * 4;
if offset_pos + 4 > data.len() {
break;
}
let tex_offset = r32(data, offset_pos) as usize;
if tex_offset + 22 > data.len() {
break;
}
let name = read_name(data, tex_offset);
let width = r16(data, tex_offset + 12) as u16;
let height = r16(data, tex_offset + 14) as u16;
let patch_count = r16(data, tex_offset + 20) as usize;
names.push(name);
let mut pixels = vec![0u8; width as usize * height as usize];
for p in 0..patch_count {
let p_off = tex_offset + 22 + p * 10;
if p_off + 10 > data.len() {
break;
}
let origin_x = r16(data, p_off) as i32;
let origin_y = r16(data, p_off + 2) as i32;
let patch_idx = r16(data, p_off + 4) as usize;
let Some(lump_idx) = patch_lump_indices.get(patch_idx).copied().flatten() else {
continue;
};
let patch_data = wad.lump_data(lump_idx);
let Some(patch) = Patch::parse(patch_data) else {
continue;
};
for col in 0..patch.width as i32 {
let tex_col = origin_x + col;
if tex_col < 0 || tex_col >= width as i32 {
continue;
}
let h = height as usize;
let dest = &mut pixels[tex_col as usize * h..(tex_col as usize + 1) * h];
let posts = &patch.columns[col as usize];
draw_posts_into_column(posts, &patch.pixel_data, dest, origin_y, height as i32);
}
}
textures.push(WallTexture {
width,
height,
data: pixels,
});
}
Ok((textures, names))
}
fn draw_posts_into_column(posts: &[Post], pixel_data: &[u8], dest: &mut [u8], origin_y: i32, height: i32) {
for post in posts {
let pixels = post.pixels(pixel_data);
let mut position = origin_y + post.top_delta as i32;
let mut source_idx = 0usize;
let mut count = pixels.len() as i32;
if position < 0 {
let skip = -position;
source_idx += skip as usize;
count -= skip;
position = 0;
}
if position + count > height {
count = height - position;
}
if count <= 0 {
continue;
}
let dest_start = position as usize;
let dest_end = dest_start + count as usize;
let src_end = source_idx + count as usize;
if src_end <= pixels.len() && dest_end <= dest.len() {
dest[dest_start..dest_end].copy_from_slice(&pixels[source_idx..src_end]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_match_basic() {
let a = *b"FLOOR0_1";
let b = *b"FLOOR0_1";
assert!(names_match(&a, &b));
}
#[test]
fn names_match_case_insensitive() {
let a = *b"Floor0_1";
let b = *b"FLOOR0_1";
assert!(names_match(&a, &b));
}
#[test]
fn names_match_null_padded() {
let a = *b"DOOR\0\0\0\0";
let b = *b"DOOR\0xxx";
assert!(names_match(&a, &b));
}
}