Skip to main content

rgpre/import/
software_texture.rs

1//! Software-renderer TEXTURE banks and palette lookup tables.
2
3use crate::{Result, error::Error};
4
5/// A software image record. Frames contain row-major palette indices.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct SoftwareTexture {
8    pub x_offset: i16,
9    pub y_offset: i16,
10    pub width: u16,
11    pub height: u16,
12    pub flags: u16,
13    pub frame_delay: u16,
14    pub scale_x: i16,
15    pub scale_y: i16,
16    pub frames: Vec<Vec<u8>>,
17}
18
19impl SoftwareTexture {
20    /// Decodes an indexed frame. Bit 8 makes palette index zero transparent.
21    pub fn rgba(&self, frame: usize, palette: Option<&super::palette::Palette>) -> Result<Vec<u8>> {
22        let pixels = self
23            .frames
24            .get(frame)
25            .ok_or_else(|| Error::Parse(format!("TEXTURE: frame {frame} outside image")))?;
26        Ok(pixels
27            .iter()
28            .flat_map(|&index| {
29                let [r, g, b] =
30                    palette.map_or([index; 3], |palette| palette.colors[usize::from(index)]);
31                [
32                    r,
33                    g,
34                    b,
35                    if index == 0 && self.flags & 0x100 != 0 {
36                        0
37                    } else {
38                        255
39                    },
40                ]
41            })
42            .collect())
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum SoftwareTextureEntry {
48    SolidColor(u8),
49    Image(SoftwareTexture),
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct SoftwareTextureBank {
54    pub name: String,
55    pub entries: Vec<SoftwareTextureEntry>,
56}
57
58fn bytes(input: &[u8], offset: usize, count: usize) -> Result<&[u8]> {
59    offset
60        .checked_add(count)
61        .and_then(|end| input.get(offset..end))
62        .ok_or_else(|| {
63            Error::Parse(format!(
64                "TEXTURE: truncated data at byte {offset}, need {count} bytes"
65            ))
66        })
67}
68fn word(input: &[u8], offset: usize) -> Result<u16> {
69    Ok(u16::from_le_bytes(
70        bytes(input, offset, 2)?.try_into().unwrap(),
71    ))
72}
73fn dword(input: &[u8], offset: usize) -> Result<usize> {
74    usize::try_from(u32::from_le_bytes(
75        bytes(input, offset, 4)?.try_into().unwrap(),
76    ))
77    .map_err(|_| Error::Parse("TEXTURE: offset exceeds address space".into()))
78}
79fn relative(input: &[u8], base: usize, offset: usize) -> Result<usize> {
80    let result = base
81        .checked_add(offset)
82        .ok_or_else(|| Error::Parse("TEXTURE: offset overflow".into()))?;
83    bytes(input, result, 0)?;
84    Ok(result)
85}
86
87/// Parses every record and animation frame, rejecting incomplete pixel data.
88pub fn parse_texture_bank(input: &[u8]) -> Result<SoftwareTextureBank> {
89    let count = usize::from(word(input, 0)?);
90    let name_bytes = bytes(input, 2, 24)?;
91    let name_end = name_bytes.iter().position(|&b| b == 0).unwrap_or(24);
92    let name = String::from_utf8_lossy(&name_bytes[..name_end])
93        .trim()
94        .to_owned();
95    bytes(input, 26, count * 20)?;
96    let mut entries = Vec::with_capacity(count);
97    let mut total_pixels = 0usize;
98    for index in 0..count {
99        let directory = 26 + index * 20;
100        let offset = dword(input, directory + 2)?;
101        let entry = if offset == 0 {
102            SoftwareTextureEntry::SolidColor(bytes(input, directory + 1, 1)?[0])
103        } else {
104            let pixels =
105                usize::from(word(input, offset + 4)?) * usize::from(word(input, offset + 6)?);
106            let decoded = pixels
107                .checked_mul(usize::from(word(input, offset + 20)?))
108                .ok_or_else(|| Error::Parse("TEXTURE: decoded pixel count overflow".into()))?;
109            total_pixels = total_pixels
110                .checked_add(decoded)
111                .ok_or_else(|| Error::Parse("TEXTURE: decoded pixel count overflow".into()))?;
112            if total_pixels > 16 * 1024 * 1024 {
113                return Err(Error::Parse(
114                    "TEXTURE: bank exceeds 16777216 decoded pixels".into(),
115                ));
116            }
117            parse_image(input, offset)
118                .map(SoftwareTextureEntry::Image)
119                .map_err(|e| Error::Parse(format!("TEXTURE record {index}: {e}")))?
120        };
121        entries.push(entry);
122    }
123    Ok(SoftwareTextureBank { name, entries })
124}
125
126fn parse_image(input: &[u8], offset: usize) -> Result<SoftwareTexture> {
127    bytes(input, offset, 28)?;
128    let width = word(input, offset + 4)?;
129    let height = word(input, offset + 6)?;
130    let flags = word(input, offset + 8)?;
131    let frame_count = usize::from(word(input, offset + 20)?);
132    let mut frames = Vec::with_capacity(frame_count);
133    if frame_count != 0 {
134        super::indexed_image::pixel_count(width, height)?;
135        let data = relative(input, offset, dword(input, offset + 14)?)?;
136        if frame_count == 1 {
137            let stride = if flags & 0x0800 != 0 {
138                256
139            } else {
140                usize::from(width)
141            };
142            if usize::from(width) > stride {
143                return Err(Error::Parse("TEXTURE: width exceeds row stride".into()));
144            }
145            let mut pixels = Vec::with_capacity(usize::from(width) * usize::from(height));
146            for row in 0..usize::from(height) {
147                pixels.extend_from_slice(bytes(input, data + row * stride, usize::from(width))?);
148            }
149            frames.push(pixels);
150        } else {
151            bytes(input, data, frame_count * 4)?;
152            for frame in 0..frame_count {
153                let start = relative(input, data, dword(input, data + frame * 4)?)?;
154                if word(input, start)? != width || word(input, start + 2)? != height {
155                    return Err(Error::Parse(format!(
156                        "TEXTURE frame {frame}: inconsistent dimensions"
157                    )));
158                }
159                let mut cursor = start + 4;
160                let mut pixels = Vec::with_capacity(usize::from(width) * usize::from(height));
161                for _ in 0..height {
162                    let mut x = 0;
163                    while x < usize::from(width) {
164                        let run = bytes(input, cursor, 2)?;
165                        cursor += 2;
166                        let transparent = usize::from(run[0]);
167                        let literal = usize::from(run[1]);
168                        if transparent + literal == 0
169                            || x + transparent + literal > usize::from(width)
170                        {
171                            return Err(Error::Parse(format!(
172                                "TEXTURE frame {frame}: invalid pixel run"
173                            )));
174                        }
175                        pixels.resize(pixels.len() + transparent, 0);
176                        pixels.extend_from_slice(bytes(input, cursor, literal)?);
177                        cursor += literal;
178                        x += transparent + literal;
179                    }
180                }
181                frames.push(pixels);
182            }
183        }
184    }
185    Ok(SoftwareTexture {
186        x_offset: word(input, offset)? as i16,
187        y_offset: word(input, offset + 2)? as i16,
188        width,
189        height,
190        flags,
191        frame_delay: word(input, offset + 22)?,
192        scale_x: word(input, offset + 24)? as i16,
193        scale_y: word(input, offset + 26)? as i16,
194        frames,
195    })
196}
197
198/// A SHADE or HAZE file's 64 rows of 256 palette-index mappings.
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct PaletteLookupTable {
201    pub rows: Vec<[u8; 256]>,
202}
203
204impl PaletteLookupTable {
205    pub fn parse(input: &[u8]) -> Result<Self> {
206        if input.len() != 16384 {
207            return Err(Error::Parse(format!(
208                "SHADE/HAZE: expected 16384 bytes, got {}",
209                input.len()
210            )));
211        }
212        Ok(Self {
213            rows: input.as_chunks::<256>().0.to_vec(),
214        })
215    }
216
217    #[must_use]
218    pub fn map(&self, row: usize, index: u8) -> Option<u8> {
219        self.rows.get(row).map(|values| values[usize::from(index)])
220    }
221}