1use anyhow::{bail, Context, Result};
5use flate2::read::DeflateDecoder;
6use std::io::Read;
7
8use crate::archive::{resource_size_from_flags, RSC7_MAGIC};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[repr(u32)]
14pub enum TextureFormat {
15 A8R8G8B8 = 21,
16 X8R8G8B8 = 22,
17 A1R5G5B5 = 25,
18 A8 = 28,
19 A8B8G8R8 = 32,
20 L8 = 50,
21 DXT1 = 0x31545844,
22 DXT3 = 0x33545844,
23 DXT5 = 0x35545844,
24 ATI1 = 0x31495441,
25 ATI2 = 0x32495441,
26 BC7 = 0x20374342,
27 Unknown = 0,
28}
29
30impl TextureFormat {
31 pub fn from_u32(v: u32) -> Self {
32 match v {
33 21 => Self::A8R8G8B8,
34 22 => Self::X8R8G8B8,
35 25 => Self::A1R5G5B5,
36 28 => Self::A8,
37 32 => Self::A8B8G8R8,
38 50 => Self::L8,
39 0x31545844 => Self::DXT1,
40 0x33545844 => Self::DXT3,
41 0x35545844 => Self::DXT5,
42 0x31495441 => Self::ATI1,
43 0x32495441 => Self::ATI2,
44 0x20374342 => Self::BC7,
45 _ => Self::Unknown,
46 }
47 }
48
49 pub fn is_block_compressed(self) -> bool {
50 matches!(self, Self::DXT1 | Self::DXT3 | Self::DXT5 | Self::ATI1 | Self::ATI2 | Self::BC7)
51 }
52}
53
54impl std::fmt::Display for TextureFormat {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 let s = match self {
57 Self::A8R8G8B8 => "A8R8G8B8",
58 Self::X8R8G8B8 => "X8R8G8B8",
59 Self::A1R5G5B5 => "A1R5G5B5",
60 Self::A8 => "A8",
61 Self::A8B8G8R8 => "A8B8G8R8",
62 Self::L8 => "L8",
63 Self::DXT1 => "DXT1",
64 Self::DXT3 => "DXT3",
65 Self::DXT5 => "DXT5",
66 Self::ATI1 => "ATI1",
67 Self::ATI2 => "ATI2",
68 Self::BC7 => "BC7",
69 Self::Unknown => "Unknown",
70 };
71 f.write_str(s)
72 }
73}
74
75#[derive(Debug)]
77pub struct YtdTexture {
78 pub name: String,
79 pub name_hash: u32,
80 pub width: u16,
81 pub height: u16,
82 pub depth: u16,
83 pub format: TextureFormat,
84 pub levels: u8,
85 pub stride: u16,
86 pub pixel_data: Vec<u8>,
87}
88
89impl YtdTexture {
90 pub fn to_dds(&self) -> Vec<u8> {
92 let mut out = Vec::new();
93 out.extend_from_slice(b"DDS ");
95
96 let has_mips = self.levels > 1;
98 let is_compressed = self.format.is_block_compressed();
99
100 let mut flags: u32 = 0x1 | 0x2 | 0x4 | 0x1000; if has_mips { flags |= 0x20000; } if is_compressed { flags |= 0x80000; } else { flags |= 0x8; } let pitch_or_linear: u32 = self.stride as u32 * self.height as u32;
105
106 out.extend_from_slice(&124u32.to_le_bytes()); out.extend_from_slice(&flags.to_le_bytes()); out.extend_from_slice(&(self.height as u32).to_le_bytes()); out.extend_from_slice(&(self.width as u32).to_le_bytes()); out.extend_from_slice(&pitch_or_linear.to_le_bytes()); out.extend_from_slice(&(self.depth as u32).to_le_bytes()); out.extend_from_slice(&(self.levels as u32).to_le_bytes()); out.extend_from_slice(&[0u8; 44]); self.write_pixelformat(&mut out);
117
118 let mut caps: u32 = 0x1000; if has_mips { caps |= 0x8 | 0x400000; } out.extend_from_slice(&caps.to_le_bytes());
121 out.extend_from_slice(&[0u8; 16]); if self.format == TextureFormat::BC7 {
125 write_dx10_header(&mut out);
129 }
130 out.extend_from_slice(&self.pixel_data);
131
132 out
133 }
134
135 fn write_pixelformat(&self, out: &mut Vec<u8>) {
136 out.extend_from_slice(&32u32.to_le_bytes()); match self.format {
138 TextureFormat::DXT1 | TextureFormat::DXT3 | TextureFormat::DXT5
139 | TextureFormat::ATI1 | TextureFormat::ATI2 => {
140 out.extend_from_slice(&0x4u32.to_le_bytes()); out.extend_from_slice(&(self.format as u32).to_le_bytes()); out.extend_from_slice(&[0u8; 20]); }
144 TextureFormat::BC7 => {
145 out.extend_from_slice(&0x4u32.to_le_bytes()); out.extend_from_slice(b"DX10"); out.extend_from_slice(&[0u8; 20]);
148 }
149 TextureFormat::A8R8G8B8 => {
150 out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&32u32.to_le_bytes()); out.extend_from_slice(&0x00FF0000u32.to_le_bytes()); out.extend_from_slice(&0x0000FF00u32.to_le_bytes()); out.extend_from_slice(&0x000000FFu32.to_le_bytes()); out.extend_from_slice(&0xFF000000u32.to_le_bytes()); }
158 TextureFormat::X8R8G8B8 => {
159 out.extend_from_slice(&0x40u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes());
161 out.extend_from_slice(&32u32.to_le_bytes());
162 out.extend_from_slice(&0x00FF0000u32.to_le_bytes());
163 out.extend_from_slice(&0x0000FF00u32.to_le_bytes());
164 out.extend_from_slice(&0x000000FFu32.to_le_bytes());
165 out.extend_from_slice(&0u32.to_le_bytes()); }
167 TextureFormat::A8B8G8R8 => {
168 out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes());
169 out.extend_from_slice(&0u32.to_le_bytes());
170 out.extend_from_slice(&32u32.to_le_bytes());
171 out.extend_from_slice(&0x000000FFu32.to_le_bytes()); out.extend_from_slice(&0x0000FF00u32.to_le_bytes()); out.extend_from_slice(&0x00FF0000u32.to_le_bytes()); out.extend_from_slice(&0xFF000000u32.to_le_bytes()); }
176 TextureFormat::A1R5G5B5 => {
177 out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes());
178 out.extend_from_slice(&0u32.to_le_bytes());
179 out.extend_from_slice(&16u32.to_le_bytes());
180 out.extend_from_slice(&0x7C00u32.to_le_bytes()); out.extend_from_slice(&0x03E0u32.to_le_bytes()); out.extend_from_slice(&0x001Fu32.to_le_bytes()); out.extend_from_slice(&0x8000u32.to_le_bytes()); }
185 TextureFormat::A8 => {
186 out.extend_from_slice(&0x2u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes());
188 out.extend_from_slice(&8u32.to_le_bytes());
189 out.extend_from_slice(&[0u8; 16]);
190 let len = out.len();
192 out[len - 4..len].copy_from_slice(&0xFFu32.to_le_bytes());
193 }
194 TextureFormat::L8 => {
195 out.extend_from_slice(&0x20000u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes());
197 out.extend_from_slice(&8u32.to_le_bytes());
198 out.extend_from_slice(&0xFFu32.to_le_bytes()); out.extend_from_slice(&[0u8; 12]);
200 }
201 TextureFormat::Unknown => {
202 out.extend_from_slice(&[0u8; 28]);
204 }
205 }
206 }
207}
208
209fn write_dx10_header(out: &mut Vec<u8>) {
210 out.extend_from_slice(&98u32.to_le_bytes()); out.extend_from_slice(&3u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&1u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); }
216
217pub fn parse_ytd(data: &[u8]) -> Result<Vec<YtdTexture>> {
223 if data.len() < 16 {
224 bail!("YTD data too short");
225 }
226
227 let magic = u32::from_le_bytes(data[0..4].try_into().unwrap());
228 if magic != RSC7_MAGIC {
229 bail!("Not an RSC7 file (magic = 0x{:08X})", magic);
230 }
231
232 let system_flags = u32::from_le_bytes(data[8..12].try_into().unwrap());
233 let graphics_flags = u32::from_le_bytes(data[12..16].try_into().unwrap());
234
235 let sys_size = resource_size_from_flags(system_flags);
236 let gfx_size = resource_size_from_flags(graphics_flags);
237 let body = &data[16..];
238
239 let decompressed = {
241 let mut out = Vec::new();
242 if DeflateDecoder::new(body).read_to_end(&mut out).is_ok() && !out.is_empty() {
243 out
244 } else {
245 body.to_vec()
247 }
248 };
249
250 if decompressed.len() < sys_size {
251 bail!(
252 "Decompressed size {} < expected system size {}",
253 decompressed.len(), sys_size
254 );
255 }
256
257 let system = &decompressed[..sys_size];
258 let graphics = if decompressed.len() >= sys_size + gfx_size {
259 &decompressed[sys_size..sys_size + gfx_size]
260 } else {
261 &decompressed[sys_size..]
262 };
263
264 let reader = ResReader { system, graphics };
265 parse_texture_dict(&reader)
266}
267
268struct ResReader<'a> {
271 system: &'a [u8],
272 graphics: &'a [u8],
273}
274
275impl<'a> ResReader<'a> {
276 fn resolve(&self, va: u64, len: usize) -> Option<&'a [u8]> {
277 if va == 0 { return None; }
278 if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
279 let off = (va - 0x50000000) as usize;
280 self.system.get(off..off + len)
281 } else if (va & 0x60000000) == 0x60000000 {
282 let off = (va - 0x60000000) as usize;
283 self.graphics.get(off..off + len)
284 } else {
285 None
286 }
287 }
288
289 fn string_at(&self, va: u64) -> Option<String> {
290 if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
291 let off = (va - 0x50000000) as usize;
292 let slice = self.system.get(off..)?;
293 let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
294 Some(String::from_utf8_lossy(&slice[..end]).into_owned())
295 } else {
296 None
297 }
298 }
299}
300
301fn u16_le(b: &[u8], off: usize) -> u16 {
304 u16::from_le_bytes(b[off..off + 2].try_into().unwrap())
305}
306fn u32_le(b: &[u8], off: usize) -> u32 {
307 u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
308}
309fn u64_le(b: &[u8], off: usize) -> u64 {
310 u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
311}
312
313fn parse_texture_dict(reader: &ResReader<'_>) -> Result<Vec<YtdTexture>> {
316 let sys = reader.system;
317 if sys.len() < 64 {
318 bail!("system section too small for TextureDictionary");
319 }
320
321 let hash_ptr = u64_le(sys, 0x20);
326 let hash_count = u32_le(sys, 0x28) as usize;
327 let tex_ptr_array = u64_le(sys, 0x30);
331 let tex_count = u32_le(sys, 0x38) as usize;
332
333 let hash_data = if hash_count > 0 {
335 reader.resolve(hash_ptr, hash_count * 4)
336 } else {
337 None
338 };
339
340 let ptr_bytes = tex_count * 8;
342 let ptr_data = if tex_count > 0 {
343 reader.resolve(tex_ptr_array, ptr_bytes)
344 .with_context(|| format!("texture pointer array out of bounds (va=0x{:X})", tex_ptr_array))?
345 } else {
346 return Ok(vec![]);
347 };
348
349 let mut textures = Vec::with_capacity(tex_count);
350 for i in 0..tex_count {
351 let tex_va = u64_le(ptr_data, i * 8);
352 if tex_va == 0 { continue; }
353
354 let name_hash = hash_data
355 .and_then(|h| h.get(i * 4..i * 4 + 4))
356 .map(|b| u32_le(b, 0))
357 .unwrap_or(0);
358
359 match parse_texture(tex_va, name_hash, reader) {
360 Ok(tex) => textures.push(tex),
361 Err(e) => eprintln!("[YTD] Warning: texture {} parse error: {}", i, e),
362 }
363 }
364
365 Ok(textures)
366}
367
368fn parse_texture(tex_va: u64, name_hash: u32, reader: &ResReader<'_>) -> Result<YtdTexture> {
369 let raw = reader.resolve(tex_va, 0x90)
371 .with_context(|| format!("texture struct out of bounds (va=0x{:X})", tex_va))?;
372
373 let name_ptr = u64_le(raw, 0x28);
375
376 let width = u16_le(raw, 0x50);
378 let height = u16_le(raw, 0x52);
379 let depth = u16_le(raw, 0x54);
380 let stride = u16_le(raw, 0x56);
381 let fmt = TextureFormat::from_u32(u32_le(raw, 0x58));
382 let levels = raw[0x5D];
383 let data_ptr = u64_le(raw, 0x70);
384
385 let name = reader.string_at(name_ptr).unwrap_or_default();
386
387 let pixel_size = calc_pixel_data_size(stride, height, levels);
389
390 let pixel_data = if pixel_size > 0 && data_ptr != 0 {
391 reader.resolve(data_ptr, pixel_size)
392 .with_context(|| format!("pixel data out of bounds (va=0x{:X}, size={})", data_ptr, pixel_size))?
393 .to_vec()
394 } else {
395 vec![]
396 };
397
398 Ok(YtdTexture { name, name_hash, width, height, depth, format: fmt, levels, stride, pixel_data })
399}
400
401fn calc_pixel_data_size(stride: u16, height: u16, levels: u8) -> usize {
402 let mut total = 0usize;
403 let mut length = stride as usize * height as usize;
404 for _ in 0..levels {
405 total += length;
406 length /= 4;
407 }
408 total
409}