use anyhow::{bail, Context, Result};
use flate2::read::DeflateDecoder;
use std::io::Read;
use crate::archive::{resource_size_from_flags, RSC7_MAGIC};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum TextureFormat {
A8R8G8B8 = 21,
X8R8G8B8 = 22,
A1R5G5B5 = 25,
A8 = 28,
A8B8G8R8 = 32,
L8 = 50,
DXT1 = 0x31545844,
DXT3 = 0x33545844,
DXT5 = 0x35545844,
ATI1 = 0x31495441,
ATI2 = 0x32495441,
BC7 = 0x20374342,
Unknown = 0,
}
impl TextureFormat {
pub fn from_u32(v: u32) -> Self {
match v {
21 => Self::A8R8G8B8,
22 => Self::X8R8G8B8,
25 => Self::A1R5G5B5,
28 => Self::A8,
32 => Self::A8B8G8R8,
50 => Self::L8,
0x31545844 => Self::DXT1,
0x33545844 => Self::DXT3,
0x35545844 => Self::DXT5,
0x31495441 => Self::ATI1,
0x32495441 => Self::ATI2,
0x20374342 => Self::BC7,
_ => Self::Unknown,
}
}
pub fn is_block_compressed(self) -> bool {
matches!(self, Self::DXT1 | Self::DXT3 | Self::DXT5 | Self::ATI1 | Self::ATI2 | Self::BC7)
}
}
impl std::fmt::Display for TextureFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::A8R8G8B8 => "A8R8G8B8",
Self::X8R8G8B8 => "X8R8G8B8",
Self::A1R5G5B5 => "A1R5G5B5",
Self::A8 => "A8",
Self::A8B8G8R8 => "A8B8G8R8",
Self::L8 => "L8",
Self::DXT1 => "DXT1",
Self::DXT3 => "DXT3",
Self::DXT5 => "DXT5",
Self::ATI1 => "ATI1",
Self::ATI2 => "ATI2",
Self::BC7 => "BC7",
Self::Unknown => "Unknown",
};
f.write_str(s)
}
}
#[derive(Debug)]
pub struct YtdTexture {
pub name: String,
pub name_hash: u32,
pub width: u16,
pub height: u16,
pub depth: u16,
pub format: TextureFormat,
pub levels: u8,
pub stride: u16,
pub pixel_data: Vec<u8>,
}
impl YtdTexture {
pub fn to_dds(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(b"DDS ");
let has_mips = self.levels > 1;
let is_compressed = self.format.is_block_compressed();
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;
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);
let mut caps: u32 = 0x1000; if has_mips { caps |= 0x8 | 0x400000; } out.extend_from_slice(&caps.to_le_bytes());
out.extend_from_slice(&[0u8; 16]);
if self.format == TextureFormat::BC7 {
write_dx10_header(&mut out);
}
out.extend_from_slice(&self.pixel_data);
out
}
fn write_pixelformat(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&32u32.to_le_bytes()); match self.format {
TextureFormat::DXT1 | TextureFormat::DXT3 | TextureFormat::DXT5
| TextureFormat::ATI1 | TextureFormat::ATI2 => {
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]); }
TextureFormat::BC7 => {
out.extend_from_slice(&0x4u32.to_le_bytes()); out.extend_from_slice(b"DX10"); out.extend_from_slice(&[0u8; 20]);
}
TextureFormat::A8R8G8B8 => {
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()); }
TextureFormat::X8R8G8B8 => {
out.extend_from_slice(&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(&0u32.to_le_bytes()); }
TextureFormat::A8B8G8R8 => {
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(&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()); }
TextureFormat::A1R5G5B5 => {
out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&16u32.to_le_bytes());
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()); }
TextureFormat::A8 => {
out.extend_from_slice(&0x2u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&8u32.to_le_bytes());
out.extend_from_slice(&[0u8; 16]);
let len = out.len();
out[len - 4..len].copy_from_slice(&0xFFu32.to_le_bytes());
}
TextureFormat::L8 => {
out.extend_from_slice(&0x20000u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&8u32.to_le_bytes());
out.extend_from_slice(&0xFFu32.to_le_bytes()); out.extend_from_slice(&[0u8; 12]);
}
TextureFormat::Unknown => {
out.extend_from_slice(&[0u8; 28]);
}
}
}
}
fn write_dx10_header(out: &mut Vec<u8>) {
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()); }
pub fn parse_ytd(data: &[u8]) -> Result<Vec<YtdTexture>> {
if data.len() < 16 {
bail!("YTD data too short");
}
let magic = u32::from_le_bytes(data[0..4].try_into().unwrap());
if magic != RSC7_MAGIC {
bail!("Not an RSC7 file (magic = 0x{:08X})", magic);
}
let system_flags = u32::from_le_bytes(data[8..12].try_into().unwrap());
let graphics_flags = u32::from_le_bytes(data[12..16].try_into().unwrap());
let sys_size = resource_size_from_flags(system_flags);
let gfx_size = resource_size_from_flags(graphics_flags);
let body = &data[16..];
let decompressed = {
let mut out = Vec::new();
if DeflateDecoder::new(body).read_to_end(&mut out).is_ok() && !out.is_empty() {
out
} else {
body.to_vec()
}
};
if decompressed.len() < sys_size {
bail!(
"Decompressed size {} < expected system size {}",
decompressed.len(), sys_size
);
}
let system = &decompressed[..sys_size];
let graphics = if decompressed.len() >= sys_size + gfx_size {
&decompressed[sys_size..sys_size + gfx_size]
} else {
&decompressed[sys_size..]
};
let reader = ResReader { system, graphics };
parse_texture_dict(&reader)
}
struct ResReader<'a> {
system: &'a [u8],
graphics: &'a [u8],
}
impl<'a> ResReader<'a> {
fn resolve(&self, va: u64, len: usize) -> Option<&'a [u8]> {
if va == 0 { return None; }
if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
let off = (va - 0x50000000) as usize;
self.system.get(off..off + len)
} else if (va & 0x60000000) == 0x60000000 {
let off = (va - 0x60000000) as usize;
self.graphics.get(off..off + len)
} else {
None
}
}
fn string_at(&self, va: u64) -> Option<String> {
if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
let off = (va - 0x50000000) as usize;
let slice = self.system.get(off..)?;
let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
Some(String::from_utf8_lossy(&slice[..end]).into_owned())
} else {
None
}
}
}
fn u16_le(b: &[u8], off: usize) -> u16 {
u16::from_le_bytes(b[off..off + 2].try_into().unwrap())
}
fn u32_le(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
}
fn u64_le(b: &[u8], off: usize) -> u64 {
u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
}
fn parse_texture_dict(reader: &ResReader<'_>) -> Result<Vec<YtdTexture>> {
let sys = reader.system;
if sys.len() < 64 {
bail!("system section too small for TextureDictionary");
}
let hash_ptr = u64_le(sys, 0x20);
let hash_count = u32_le(sys, 0x28) as usize;
let tex_ptr_array = u64_le(sys, 0x30);
let tex_count = u32_le(sys, 0x38) as usize;
let hash_data = if hash_count > 0 {
reader.resolve(hash_ptr, hash_count * 4)
} else {
None
};
let ptr_bytes = tex_count * 8;
let ptr_data = if tex_count > 0 {
reader.resolve(tex_ptr_array, ptr_bytes)
.with_context(|| format!("texture pointer array out of bounds (va=0x{:X})", tex_ptr_array))?
} else {
return Ok(vec![]);
};
let mut textures = Vec::with_capacity(tex_count);
for i in 0..tex_count {
let tex_va = u64_le(ptr_data, i * 8);
if tex_va == 0 { continue; }
let name_hash = hash_data
.and_then(|h| h.get(i * 4..i * 4 + 4))
.map(|b| u32_le(b, 0))
.unwrap_or(0);
match parse_texture(tex_va, name_hash, reader) {
Ok(tex) => textures.push(tex),
Err(e) => eprintln!("[YTD] Warning: texture {} parse error: {}", i, e),
}
}
Ok(textures)
}
fn parse_texture(tex_va: u64, name_hash: u32, reader: &ResReader<'_>) -> Result<YtdTexture> {
let raw = reader.resolve(tex_va, 0x90)
.with_context(|| format!("texture struct out of bounds (va=0x{:X})", tex_va))?;
let name_ptr = u64_le(raw, 0x28);
let width = u16_le(raw, 0x50);
let height = u16_le(raw, 0x52);
let depth = u16_le(raw, 0x54);
let stride = u16_le(raw, 0x56);
let fmt = TextureFormat::from_u32(u32_le(raw, 0x58));
let levels = raw[0x5D];
let data_ptr = u64_le(raw, 0x70);
let name = reader.string_at(name_ptr).unwrap_or_default();
let pixel_size = calc_pixel_data_size(stride, height, levels);
let pixel_data = if pixel_size > 0 && data_ptr != 0 {
reader.resolve(data_ptr, pixel_size)
.with_context(|| format!("pixel data out of bounds (va=0x{:X}, size={})", data_ptr, pixel_size))?
.to_vec()
} else {
vec![]
};
Ok(YtdTexture { name, name_hash, width, height, depth, format: fmt, levels, stride, pixel_data })
}
fn calc_pixel_data_size(stride: u16, height: u16, levels: u8) -> usize {
let mut total = 0usize;
let mut length = stride as usize * height as usize;
for _ in 0..levels {
total += length;
length /= 4;
}
total
}