quake_util/lump/
repr.rs

1use crate::error;
2use crate::lump::kind;
3use crate::slice_to_cstring;
4use crate::Palette;
5use std::boxed::Box;
6use std::ffi::{CString, IntoStringError};
7use std::mem::size_of;
8use std::string::{String, ToString};
9
10/// Enum w/ variants for each known lump kind
11#[derive(Clone, PartialEq, Eq, Debug)]
12pub enum Lump {
13    Palette(Box<Palette>),
14    StatusBar(Image),
15    MipTexture(MipTexture),
16    Flat(Box<[u8]>),
17}
18
19impl Lump {
20    /// Single-byte lump kind identifier as used in WAD entries
21    pub fn kind(&self) -> u8 {
22        match self {
23            Self::Palette(_) => kind::PALETTE,
24            Self::StatusBar(_) => kind::SBAR,
25            Self::MipTexture(_) => kind::MIPTEX,
26            _ => kind::FLAT,
27        }
28    }
29}
30
31/// Image stored as palette indices (0..256) in row-major order
32#[derive(Clone, PartialEq, Eq, Debug)]
33pub struct Image {
34    width: u32,
35    height: u32,
36    pixels: Box<[u8]>,
37}
38
39impl Image {
40    /// Create an image from a width and list of pixels.  Height is calculated
41    /// from number of pixels and width, or 0 if pixels are empty.
42    ///
43    /// # Panics
44    ///
45    /// Panics if pixel count does not fit within a `u32`, pixels cannot fit
46    /// within an integer number of row, or there are a non-zero number of
47    /// pixels and width is 0.
48    pub fn from_pixels(width: u32, pixels: Box<[u8]>) -> Self {
49        let pixel_ct: u32 = pixels.len().try_into().expect("Too many pixels");
50
51        if pixels.is_empty() {
52            return Image {
53                width: 0,
54                height: 0,
55                pixels,
56            };
57        }
58
59        if width == 0 {
60            panic!("Image with pixels must have width > 0");
61        }
62
63        if pixel_ct % width != 0 {
64            panic!("Incomplete pixel row");
65        }
66
67        Image {
68            width,
69            height: pixel_ct / width,
70            pixels,
71        }
72    }
73
74    pub fn width(&self) -> u32 {
75        self.width
76    }
77
78    pub fn height(&self) -> u32 {
79        self.height
80    }
81
82    /// Slice of all the pixels
83    pub fn pixels(&self) -> &[u8] {
84        &self.pixels[..]
85    }
86}
87
88/// Mip-mapped texture.  Contains exactly 4 mips (including the full resolution
89/// image).
90///
91/// Textures mips are guaranteed to be valid, meaning that width and height of
92/// a mip is half that of the width and height of the previous mip.
93#[derive(Clone, PartialEq, Eq, Debug)]
94pub struct MipTexture {
95    name: [u8; 16],
96    mips: [Image; 4],
97}
98
99impl MipTexture {
100    pub const MIP_COUNT: usize = 4;
101
102    /// Assemble a texture from provided mips with the given name.  Useful if
103    /// name is already given by a WAD entry as a block of 16 bytes.
104    ///
105    /// # Panic
106    ///
107    /// Will panic if mips are not valid.
108    pub fn from_parts(name: [u8; 16], mips: [Image; Self::MIP_COUNT]) -> Self {
109        Self::validate_mips(&mips);
110        MipTexture { name, mips }
111    }
112
113    /// Assemble a texture from provided mips with `name` converted to a block
114    /// of 16 bytes.
115    ///
116    /// @ Panic
117    ///
118    /// Will panic if mips are not valid or `name` does not fit within 16 bytes.
119    pub fn new(name: String, mips: [Image; Self::MIP_COUNT]) -> Self {
120        let mut name_field = [0u8; 16];
121        let name_bytes = &name.into_bytes();
122        name_field[..name_bytes.len()].copy_from_slice(name_bytes);
123        let name = name_field;
124        Self::validate_mips(&mips);
125
126        MipTexture { name, mips }
127    }
128
129    fn validate_mips(mips: &[Image; Self::MIP_COUNT]) {
130        for l in 0..(Self::MIP_COUNT - 1) {
131            let r = l + 1;
132
133            if Some(mips[l].width) != mips[r].width.checked_mul(2) {
134                panic!("Bad mipmaps");
135            }
136
137            if Some(mips[l].height) != mips[r].height.checked_mul(2) {
138                panic!("Bad mipmaps");
139            }
140        }
141    }
142
143    /// Obtain the name as a C string.  If the name is not already
144    /// null-terminated (in which case the entry is not well-formed) a null byte
145    /// is appended to make a valid C string.
146    pub fn name_to_cstring(&self) -> CString {
147        slice_to_cstring(&self.name)
148    }
149
150    /// Attempt to interpret the name as UTF-8 encoded string
151    pub fn name_to_string(&self) -> Result<String, IntoStringError> {
152        self.name_to_cstring().into_string()
153    }
154
155    pub fn name(&self) -> [u8; 16] {
156        self.name
157    }
158
159    /// Get the texture mip as an image at the specified index.
160    ///
161    /// # Panic
162    ///
163    /// Panics if index is > 3
164    pub fn mip(&self, index: usize) -> &Image {
165        if index < Self::MIP_COUNT {
166            &self.mips[index]
167        } else {
168            panic!("Outside mip bounds ([0..{}])", Self::MIP_COUNT);
169        }
170    }
171
172    /// Get the texture mips as a slice of images
173    pub fn mips(&self) -> &[Image] {
174        &self.mips[..]
175    }
176}
177
178/// Lump header for mip-mapped textures
179#[derive(Clone, Copy, PartialEq, Eq, Debug)]
180#[repr(C, packed)]
181pub struct MipTextureHead {
182    pub(crate) name: [u8; 16],
183    pub(crate) width: u32,
184    pub(crate) height: u32,
185    pub(crate) offsets: [u32; 4],
186}
187
188impl TryFrom<[u8; size_of::<MipTextureHead>()]> for MipTextureHead {
189    type Error = error::BinParse;
190
191    /// Obtain header from a block of bytes as found in a miptex WAD lump.
192    ///
193    /// # Panic
194    ///
195    /// Will panic if width or height are not each divisible by 8, in which case
196    /// valid mips cannot be generated.  Will panic if number of pixels in mip
197    /// 0 cannot fit within a `u32`.
198    fn try_from(
199        bytes: [u8; size_of::<MipTextureHead>()],
200    ) -> Result<Self, Self::Error> {
201        let name = <[u8; 16]>::try_from(&bytes[..16]).unwrap();
202
203        let bytes = &bytes[16..];
204
205        let width =
206            u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[..4]).unwrap());
207
208        let bytes = &bytes[4..];
209
210        let height =
211            u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[..4]).unwrap());
212
213        if width % 8 != 0 {
214            return Err(error::BinParse::Parse(format!(
215                "Invalid width {}",
216                width
217            )));
218        }
219
220        if height % 8 != 0 {
221            return Err(error::BinParse::Parse(format!(
222                "Invalid height {}",
223                height
224            )));
225        }
226
227        width
228            .checked_mul(height)
229            .ok_or(error::BinParse::Parse("Texture too large".to_string()))?;
230
231        let bytes = &bytes[4..];
232
233        let mut offsets = [0u32; 4];
234
235        for i in 0..4 {
236            offsets[i] = u32::from_le_bytes(
237                <[u8; 4]>::try_from(&bytes[(4 * i)..(4 * i + 4)]).unwrap(),
238            );
239        }
240
241        Ok(MipTextureHead {
242            name,
243            width,
244            height,
245            offsets,
246        })
247    }
248}