1use 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 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}