tiger_lib/
dds.rs

1//! Validator for the `.dds` (picture) files that are used in the game.
2
3use std::fs::{File, metadata};
4use std::io::{Read, Result};
5use std::path::PathBuf;
6
7use crate::fileset::{FileEntry, FileHandler};
8use crate::helpers::TigerHashMap;
9use crate::parse::ParserMemory;
10use crate::report::{ErrorKey, err, tips, warn};
11#[cfg(feature = "ck3")]
12use crate::token::Token;
13
14const DDS_HEADER_SIZE: usize = 124;
15const DDS_HEIGHT_OFFSET: usize = 12;
16const DDS_WIDTH_OFFSET: usize = 16;
17const DDS_PIXELFORMAT_FLAGS_OFFSET: usize = 80;
18
19fn from_le32(buffer: &[u8], offset: usize) -> u32 {
20    u32::from(buffer[offset])
21        | (u32::from(buffer[offset + 1]) << 8)
22        | (u32::from(buffer[offset + 2]) << 16)
23        | (u32::from(buffer[offset + 3]) << 24)
24}
25
26#[derive(Clone, Debug, Default)]
27pub struct DdsFiles {
28    dds_files: TigerHashMap<String, DdsInfo>,
29}
30
31impl DdsFiles {
32    fn load_dds(entry: &FileEntry) -> Result<Option<DdsInfo>> {
33        if metadata(entry.fullpath())?.len() == 0 {
34            warn(ErrorKey::ImageFormat).msg("empty file").loc(entry).push();
35            return Ok(None);
36        }
37        let mut f = File::open(entry.fullpath())?;
38        let mut buffer = [0; DDS_HEADER_SIZE];
39        f.read_exact(&mut buffer)?;
40        if buffer.starts_with(b"\x89PNG") {
41            let msg = "actually a PNG";
42            let info =
43                "this may be slower and lower quality because PNG format does not have mipmaps";
44            tips(ErrorKey::ImageFormat).msg(msg).info(info).loc(entry).push();
45            return Ok(None);
46        }
47        if !buffer.starts_with(b"DDS ") {
48            err(ErrorKey::ImageFormat).msg("not a DDS file").loc(entry).push();
49            return Ok(None);
50        }
51        Ok(Some(DdsInfo::new(entry.clone(), &buffer)))
52    }
53
54    fn handle_dds(&mut self, entry: &FileEntry, info: DdsInfo) {
55        self.dds_files.insert(entry.path().to_string_lossy().to_string(), info);
56    }
57
58    pub fn validate(&self) {
59        for item in self.dds_files.values() {
60            item.validate();
61        }
62    }
63
64    #[cfg(feature = "ck3")]
65    pub fn validate_frame(&self, key: &Token, width: u32, height: u32, frame: u32) {
66        // Note: `frame` is 1-based
67        if let Some(info) = self.dds_files.get(key.as_str()) {
68            if info.height != height {
69                let msg = format!("texture does not match frame height of {height}");
70                warn(ErrorKey::ImageFormat).msg(msg).loc(key).push();
71            } else if width == 0 || (info.width % width) != 0 {
72                let msg = format!("texture is not a multiple of frame width {width}");
73                warn(ErrorKey::ImageFormat).msg(msg).loc(key).push();
74            } else if frame * width > info.width {
75                let msg = format!("texture is not large enough for frame index {frame}");
76                warn(ErrorKey::ImageFormat).msg(msg).loc(key).push();
77            }
78        }
79    }
80}
81
82impl FileHandler<DdsInfo> for DdsFiles {
83    fn subpath(&self) -> PathBuf {
84        PathBuf::from("gfx")
85    }
86
87    fn load_file(&self, entry: &FileEntry, _parser: &ParserMemory) -> Option<DdsInfo> {
88        if !entry.filename().to_string_lossy().ends_with(".dds") {
89            return None;
90        }
91
92        match Self::load_dds(entry) {
93            Ok(info) => info,
94            Err(e) => {
95                err(ErrorKey::ReadError)
96                    .msg("could not read dds header")
97                    .info(format!("{e:#}"))
98                    .loc(entry)
99                    .push();
100                None
101            }
102        }
103    }
104
105    fn handle_file(&mut self, entry: &FileEntry, info: DdsInfo) {
106        self.handle_dds(entry, info);
107    }
108}
109
110#[derive(Clone, Debug)]
111pub struct DdsInfo {
112    entry: FileEntry,
113    compressed: bool,
114    width: u32,
115    height: u32,
116}
117
118impl DdsInfo {
119    pub fn new(entry: FileEntry, header: &[u8]) -> Self {
120        Self {
121            entry,
122            compressed: (from_le32(header, DDS_PIXELFORMAT_FLAGS_OFFSET) & 0x04) != 0,
123            width: from_le32(header, DDS_WIDTH_OFFSET),
124            height: from_le32(header, DDS_HEIGHT_OFFSET),
125        }
126    }
127
128    fn validate(&self) {
129        if self.compressed && !(self.width % 4 == 0 || self.height % 4 == 0) {
130            let msg = "compressed DDS must have width and height divisible by 4";
131            let info = format!(
132                "DDS file is {}x{}, which can cause scaling problems and graphical artifacts",
133                self.width, self.height
134            );
135            err(ErrorKey::ImageSize).msg(msg).info(info).loc(&self.entry).push();
136        }
137    }
138}