diff --git a/Cargo.lock b/Cargo.lock index 29626e0..3a5dc7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,21 +221,6 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" -[[package]] -name = "paktool-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "argh", - "binrw", - "binrw_derive", - "byteorder", - "env_logger", - "log", - "memmap2", - "uuid", -] - [[package]] name = "proc-macro2" version = "1.0.51" @@ -271,6 +256,21 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "retrotool" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "binrw", + "binrw_derive", + "byteorder", + "env_logger", + "log", + "memmap2", + "uuid", +] + [[package]] name = "rustix" version = "0.36.8" diff --git a/Cargo.toml b/Cargo.toml index 06e4808..b846e53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "paktool-rs" +name = "retrotool" version = "0.1.0" edition = "2021" diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 39f5341..6a17d7c 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1 +1,11 @@ pub mod pak; +pub mod txtr; + +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +pub enum SubCommand { + Pak(pak::Args), + Txtr(txtr::Args), +} diff --git a/src/cmd/pak.rs b/src/cmd/pak.rs index 54abb14..ef717b5 100644 --- a/src/cmd/pak.rs +++ b/src/cmd/pak.rs @@ -1,18 +1,31 @@ use std::{ borrow::Cow, - fmt::{Debug, Display, Formatter, Write}, + collections::HashMap, + fmt::Debug, fs, - io::{Cursor, Read, Seek, SeekFrom}, + fs::File, + io::{BufWriter, Cursor, Read, Seek, SeekFrom, Write}, path::PathBuf, }; -use anyhow::{anyhow, bail, ensure, Result}; +use anyhow::{bail, ensure, Context, Result}; use argh::FromArgs; -use binrw::{BinReaderExt, BinResult, Endian}; -use binrw_derive::binrw; +use binrw::{binrw, BinReaderExt, BinResult, BinWriterExt, Endian}; use uuid::Uuid; -use crate::util::{file::map_file, lzss::decompress}; +use crate::{ + array_ref, + format::{ + adir::{AssetDirectory, AssetDirectoryEntry, K_CHUNK_ADIR}, + chunk::{ChunkDescriptor, ChunkType}, + meta::{Metadata, MetadataEntry}, + peek_four_cc, + rfrm::{FormDescriptor, K_CHUNK_RFRM, K_FORM_PAK, K_FORM_TOC}, + strg::{StringTable, StringTableEntry, K_CHUNK_STRG}, + FourCC, + }, + util::{file::map_file, lzss::decompress}, +}; #[derive(FromArgs, PartialEq, Debug)] /// process PAK files @@ -26,6 +39,7 @@ pub struct Args { #[argh(subcommand)] enum SubCommand { Extract(ExtractArgs), + Package(PackageArgs), } #[derive(FromArgs, PartialEq, Eq, Debug)] @@ -40,214 +54,32 @@ pub struct ExtractArgs { output: PathBuf, } -#[binrw] -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct FourCC(pub [u8; 4]); - -impl FourCC { - #[inline] - const fn swap(self) -> Self { Self([self.0[3], self.0[2], self.0[1], self.0[0]]) } -} - -impl Display for FourCC { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for c in self.0 { - f.write_char(c as char)?; - } - Ok(()) - } -} - -impl Debug for FourCC { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_char('"')?; - for c in self.0 { - f.write_char(c as char)?; - } - f.write_char('"')?; - Ok(()) - } -} - -impl PartialEq<[u8; 4]> for FourCC { - fn eq(&self, other: &[u8; 4]) -> bool { &self.0 == other } -} - -// PAK file -const K_FORM_PAK: FourCC = FourCC(*b"PACK"); -// Table of contents -const K_FORM_TOC: FourCC = FourCC(*b"TOCC"); - -#[binrw] -#[brw(magic = b"RFRM")] -#[derive(Clone, Debug)] -pub struct FormDescriptor { - pub size: u64, - pub unk: u64, - pub id: FourCC, - pub version: u32, - pub other_version: u32, // ? -} - -impl FormDescriptor { - #[inline] - fn read(reader: &mut R, e: Endian) -> BinResult { reader.read_type(e) } - - #[inline] - fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { - let mut reader = Cursor::new(data); - let header = Self::read(&mut reader, e)?; - let start = reader.position(); - let slice = &data[start as usize..(start + header.size) as usize]; - let remain = &data[(start + header.size) as usize..]; - Ok((header, slice, remain)) - } -} - -// Asset directory -const K_CHUNK_ADIR: FourCC = FourCC(*b"ADIR"); -// Metadata -const K_CHUNK_META: FourCC = FourCC(*b"META"); -// String table -const K_CHUNK_STRG: FourCC = FourCC(*b"STRG"); - -#[binrw] -#[derive(Clone, Debug)] -pub struct ChunkDescriptor { - pub id: FourCC, - pub size: u64, - pub unk: u32, - // game skips this amount of bytes before continuing - // but always 0? - pub skip: u64, -} - -impl ChunkDescriptor { - #[inline] - fn read(reader: &mut R, e: Endian) -> BinResult { - let desc: ChunkDescriptor = reader.read_type(e)?; - reader.seek(SeekFrom::Current(desc.skip as i64))?; - Ok(desc) - } - - #[inline] - fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { - let mut reader = Cursor::new(data); - let header = Self::read(&mut reader, e)?; - let start = reader.position(); - let slice = &data[start as usize..(start + header.size) as usize]; - let remain = &data[(start + header.size) as usize..]; - Ok((header, slice, remain)) - } -} - -#[derive(Clone, Debug)] -enum ChunkType { - AssetDirectory(AssetDirectory), - Metadata(Metadata), - StringTable(StringTable), -} - -impl ChunkType { - #[inline] - fn read(data: &[u8], kind: FourCC, e: Endian) -> Result { - let mut reader = Cursor::new(data); - match kind { - K_CHUNK_ADIR => Ok(Self::AssetDirectory(reader.read_type(e)?)), - K_CHUNK_META => Ok(Self::Metadata(reader.read_type(e)?)), - K_CHUNK_STRG => Ok(Self::StringTable(reader.read_type(e)?)), - _ => Err(anyhow!("Unknown chunk type {:?}", kind)), - } - } -} - -#[binrw] -#[derive(Clone, Debug)] -struct AssetDirectory { - #[bw(try_calc = entries.len().try_into())] - entry_count: u32, - #[br(count = entry_count)] - entries: Vec, -} - -#[binrw] -#[derive(Clone, Debug)] -pub struct AssetDirectoryEntry { - pub asset_type: FourCC, - #[br(map = Uuid::from_u128)] - #[bw(map = Uuid::as_u128)] - pub asset_id: Uuid, - pub unk1: u32, - pub unk2: u32, - pub offset: u64, - pub decompressed_size: u64, - pub size: u64, -} - -#[binrw] -#[derive(Clone, Debug)] -pub struct Metadata { - #[bw(try_calc = entries.len().try_into())] - entry_count: u32, - #[br(count = entry_count)] - entries: Vec, -} - -#[binrw] -#[derive(Clone, Debug)] -pub struct MetadataEntry { - #[br(map = Uuid::from_u128)] - #[bw(map = Uuid::as_u128)] - pub asset_id: Uuid, - pub offset: u32, -} - -#[binrw] -#[derive(Clone, Debug)] -pub struct StringTable { - #[bw(try_calc = entries.len().try_into())] - entry_count: u32, - #[br(count = entry_count)] - entries: Vec, -} - -impl StringTable { - fn name_for_uuid(&self, id: Uuid) -> Option { - self.entries - .iter() - .find(|e| e.asset_id == id) - .map(|e| String::from_utf8(e.name.clone()).unwrap()) - } -} - -#[binrw] -#[derive(Clone, Debug)] -pub struct StringTableEntry { - #[br(map = FourCC::swap)] - #[bw(map = |&f| f.swap())] - pub kind: FourCC, - #[br(map = Uuid::from_u128)] - #[bw(map = Uuid::as_u128)] - pub asset_id: Uuid, - #[bw(try_calc = name.len().try_into())] - pub name_length: u32, - #[br(count = name_length)] - pub name: Vec, +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// package a PAK file +#[argh(subcommand, name = "package")] +pub struct PackageArgs { + #[argh(positional)] + /// input directory + input: PathBuf, + #[argh(positional)] + /// output file + output: PathBuf, } pub fn run(args: Args) -> Result<()> { match args.command { SubCommand::Extract(c_args) => extract(c_args), + SubCommand::Package(c_args) => package(c_args), } } /// Recursively dump an RFRM + contained chunks -fn dump_rfrm<'a, W: std::io::Write>(w: &mut W, data: &'a [u8], indent: usize) -> Result<&'a [u8]> { +fn dump_rfrm<'a, W: Write>(w: &mut W, data: &'a [u8], indent: usize) -> Result<&'a [u8]> { let (rfrm, mut rfrm_data, remain) = FormDescriptor::slice(data, Endian::Little)?; let indstr = " ".repeat(indent); writeln!(w, "{indstr}{rfrm:?}")?; while !rfrm_data.is_empty() { - if rfrm_data[0..4] == *b"RFRM" { + if peek_four_cc(rfrm_data) == K_CHUNK_RFRM { rfrm_data = dump_rfrm(w, rfrm_data, indent + 1)?; } else { let (desc, chunk_data, remain) = ChunkDescriptor::slice(rfrm_data, Endian::Little)?; @@ -258,81 +90,349 @@ fn dump_rfrm<'a, W: std::io::Write>(w: &mut W, data: &'a [u8], indent: usize) -> Ok(remain) } +#[binrw] +#[derive(Clone, Debug)] +pub struct AssetInfo { + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + id: Uuid, + compression_type: u32, +} + +#[derive(Debug, Clone)] +struct Asset<'a> { + id: Uuid, + kind: FourCC, + name: Option, + // TODO lazy decompression? + data: Cow<'a, [u8]>, + meta: Option>, + info: AssetInfo, + version: u32, + other_version: u32, +} + +#[derive(Debug, Clone)] +struct Package<'a> { + assets: Vec>, +} + +impl Package<'_> { + fn read(data: &[u8], e: Endian) -> Result { + let (pack, pack_data, _) = FormDescriptor::slice(data, e)?; + ensure!(pack.id == K_FORM_PAK); + ensure!(pack.version == 1); + // log::info!("PACK: {:?}", pack); + let (tocc, mut tocc_data, _) = FormDescriptor::slice(pack_data, e)?; + ensure!(tocc.id == K_FORM_TOC); + ensure!(tocc.version == 3); + // log::info!("TOCC: {:?}", tocc); + let mut adir: Option = None; + let mut meta: HashMap = HashMap::new(); + let mut strg: Option = None; + while !tocc_data.is_empty() { + let (desc, chunk_data, remain) = ChunkDescriptor::slice(tocc_data, e)?; + // log::info!("{:?} data size {}", desc, chunk_data.len()); + let header = ChunkType::read(chunk_data, desc.id, e)?; + match header { + ChunkType::AssetDirectory(chunk) => { + // for entry in &chunk.entries { + // log::info!("- {:?}", entry); + // } + adir = Some(chunk); + } + ChunkType::Metadata(chunk) => { + let mut iter = chunk.entries.iter().peekable(); + while let Some(entry) = iter.next() { + let size = if let Some(next) = iter.peek() { + (next.offset - entry.offset) as usize + } else { + chunk_data.len() - entry.offset as usize + }; + // log::info!("- {:?} (size {:#X})", entry, size); + meta.insert( + entry.asset_id, + &chunk_data[entry.offset as usize..entry.offset as usize + size], + ); + } + } + ChunkType::StringTable(chunk) => { + // for entry in &chunk.entries { + // log::info!("- {:?}", entry); + // } + strg = Some(chunk); + } + } + tocc_data = remain; + } + // log::info!("Remaining PACK data: {:#X}", pack_remain.len()); + + let mut package = Package { assets: vec![] }; + if let Some(adir) = adir { + for asset_entry in adir.entries { + let name = + strg.as_ref().and_then(|table| table.name_for_uuid(asset_entry.asset_id)); + + let mut compression_type = 0u32; + let data: Cow<[u8]> = if asset_entry.size != asset_entry.decompressed_size { + let compression_bytes = + &data[asset_entry.offset as usize..asset_entry.offset as usize + 4]; + compression_type = u32::from_le_bytes(compression_bytes.try_into().unwrap()); + // log::info!("Decompressing {}", compression_type); + let mut out = vec![0u8; asset_entry.decompressed_size as usize]; + let data = &data[asset_entry.offset as usize + 4 + ..(asset_entry.offset + asset_entry.size) as usize]; + match compression_type { + 1 => decompress::<1>(data, &mut out), + 2 => decompress::<2>(data, &mut out), + 3 => decompress::<3>(data, &mut out), + _ => bail!("Unsupported compression mode {}", compression_type), + } + Cow::Owned(out) + } else { + Cow::Borrowed( + &data[asset_entry.offset as usize + ..(asset_entry.offset + asset_entry.size) as usize], + ) + }; + + // Validate RFRM + { + let (form, _, _) = FormDescriptor::slice(&data, Endian::Little)?; + ensure!(asset_entry.version == form.version); + ensure!(asset_entry.other_version == form.other_version); + ensure!(asset_entry.decompressed_size == form.size + 32); + } + + package.assets.push(Asset { + id: asset_entry.asset_id, + kind: asset_entry.asset_type, + name, + data, + meta: meta.get(&asset_entry.asset_id).map(|data| Cow::Borrowed(*data)), + info: AssetInfo { id: asset_entry.asset_id, compression_type }, + version: asset_entry.version, + other_version: asset_entry.other_version, + }); + } + } else { + bail!("Failed to locate asset directory"); + } + Ok(package) + } + + fn write(&self, w: &mut W, e: Endian) -> Result<()> { + let mut asset_directory = AssetDirectory::default(); + let mut metadata = Metadata::default(); + let mut string_table = StringTable::default(); + for asset in &self.assets { + asset_directory.entries.push(AssetDirectoryEntry { + asset_type: asset.kind, + asset_id: asset.id, + version: asset.version, + other_version: asset.other_version, + offset: 0, + decompressed_size: asset.data.len() as u64, + size: asset.data.len() as u64, + }); + if asset.meta.is_some() { + metadata.entries.push(MetadataEntry { asset_id: asset.id, offset: 0 }); + } + if let Some(name) = &asset.name { + string_table.entries.push(StringTableEntry { + kind: asset.kind, + asset_id: asset.id, + name: name.as_bytes().to_vec(), + }); + } + } + let mut adir_offset = 0; + FormDescriptor { size: 0, unk1: 0, id: K_FORM_PAK, version: 1, other_version: 1 }.write( + w, + e, + |w| { + FormDescriptor { size: 0, unk1: 0, id: K_FORM_TOC, version: 3, other_version: 3 } + .write(w, e, |w| { + ChunkDescriptor { id: K_CHUNK_ADIR, size: 0, unk: 1, skip: 0 }.write( + w, + e, + |w| { + adir_offset = w.stream_position()?; + w.write_type(&asset_directory, e)?; + Ok(()) + }, + )?; + ChunkDescriptor { id: K_CHUNK_META, size: 0, unk: 1, skip: 0 }.write( + w, + e, + |w| { + let start = w.stream_position()?; + w.write_type(&metadata, e)?; + for (asset, entry) in self + .assets + .iter() + .filter(|a| a.meta.is_some()) + .zip(&mut metadata.entries) + { + entry.offset = (w.stream_position()? - start) as u32; + w.write_all(asset.meta.as_ref().unwrap())?; + } + let end = w.stream_position()?; + w.seek(SeekFrom::Start(start))?; + w.write_type(&metadata, e)?; + w.seek(SeekFrom::Start(end))?; + Ok(()) + }, + )?; + ChunkDescriptor { id: K_CHUNK_STRG, size: 0, unk: 1, skip: 0 }.write( + w, + e, + |w| { + w.write_type(&string_table, e)?; + Ok(()) + }, + )?; + Ok(()) + })?; + for (asset, entry) in self.assets.iter().zip(&mut asset_directory.entries) { + entry.offset = w.stream_position()?; + w.write_all(&asset.data)?; + } + Ok(()) + }, + )?; + // Write updated offsets + w.seek(SeekFrom::Start(adir_offset))?; + w.write_type(&asset_directory, e)?; + Ok(()) + } +} + fn extract(args: ExtractArgs) -> Result<()> { let data = map_file(args.input)?; - let (pack, pack_data, _) = FormDescriptor::slice(&data, Endian::Little)?; - ensure!(pack.id == K_FORM_PAK); - ensure!(pack.version == 1); - log::info!("PACK: {:?}", pack); - let (tocc, mut tocc_data, pack_remain) = FormDescriptor::slice(pack_data, Endian::Little)?; - ensure!(tocc.id == K_FORM_TOC); - ensure!(tocc.version == 3); - log::info!("TOCC: {:?}", tocc); - let mut adir: Option = None; - let mut meta: Option = None; - let mut strg: Option = None; - while !tocc_data.is_empty() { - let (desc, chunk_data, remain) = ChunkDescriptor::slice(tocc_data, Endian::Little)?; - log::info!("{:?} data size {}", desc, chunk_data.len()); - let header = ChunkType::read(chunk_data, desc.id, Endian::Little)?; - match header { - ChunkType::AssetDirectory(chunk) => { - for entry in &chunk.entries { - log::info!("- {:?}", entry); - } - adir = Some(chunk); - } - ChunkType::Metadata(chunk) => { - for entry in &chunk.entries { - log::info!("- {:?}", entry); - } - meta = Some(chunk); - } - ChunkType::StringTable(chunk) => { - for entry in &chunk.entries { - log::info!("- {:?}", entry); - } - strg = Some(chunk); - } + let package = Package::read(&data, Endian::Little)?; + for asset in &package.assets { + let name = asset + .name + .as_ref() + .map(|name| format!("{} ({})", asset.id, name)) + .unwrap_or_else(|| format!("{}", asset.id)); + log::info!( + "Asset {} {} size {:#X} (compressed {}, meta size {:#X})", + asset.kind, + name, + asset.data.len(), + asset.data.is_owned(), + asset.meta.as_ref().map(|m| m.len()).unwrap_or_default() + ); + let file_name = asset + .name + .as_ref() + .map(|name| format!("{}.{}", name, asset.kind)) + .unwrap_or_else(|| format!("{}.{}", asset.id, asset.kind)); + let path = args.output.join(&file_name); + + let mut file = BufWriter::new( + File::create(&path) + .with_context(|| format!("Failed to create file '{}'", path.display()))?, + ); + file.write_all(&asset.data)?; + + // Write custom footer + let form_pos = file.stream_position()?; + let mut form = + FormDescriptor { size: 0, unk1: 0, id: FourCC(*b"FOOT"), version: 1, other_version: 1 }; + file.write_le(&form)?; + let data_pos = file.stream_position()?; + { + let ainf_chunk = ChunkDescriptor { id: FourCC(*b"AINF"), size: 20, unk: 0, skip: 0 }; + file.write_le(&ainf_chunk)?; + file.write_le(&asset.info)?; } - tocc_data = remain; - } - log::info!("Remaining PACK data: {:#X}", pack_remain.len()); - if let Some(adir) = adir { - for asset_entry in adir.entries { - let name = strg - .as_ref() - .and_then(|table| table.name_for_uuid(asset_entry.asset_id)) - .unwrap_or_else(|| format!("{}", asset_entry.asset_id)); - let filename = format!("{}.{}", name, asset_entry.asset_type); - let path = args.output.join(filename); - log::info!("Extracting {} ({:?})", path.display(), asset_entry); - let data: Cow<[u8]> = if asset_entry.size != asset_entry.decompressed_size { - let compression_bytes = - &data[asset_entry.offset as usize..asset_entry.offset as usize + 4]; - let compression_type = u32::from_le_bytes(compression_bytes.try_into().unwrap()); - log::info!("Decompressing {}", compression_type); - let mut out = vec![0u8; asset_entry.decompressed_size as usize]; - let data = &data[asset_entry.offset as usize + 4 - ..(asset_entry.offset + asset_entry.size) as usize]; - match compression_type { - 1 => decompress::<1>(data, &mut out), - 2 => decompress::<2>(data, &mut out), - 3 => decompress::<3>(data, &mut out), - _ => bail!("Unsupported compression mode {}", compression_type), - } - Cow::Owned(out) - } else { - Cow::Borrowed( - &data[asset_entry.offset as usize - ..(asset_entry.offset + asset_entry.size) as usize], - ) - }; - // TODO strip RFRM header? - fs::write(path, &data)?; + if let Some(meta) = &asset.meta { + let meta_chunk = + ChunkDescriptor { id: FourCC(*b"META"), size: meta.len() as u64, unk: 0, skip: 0 }; + file.write_le(&meta_chunk)?; + file.write_all(meta)?; } - } else { - bail!("Failed to locate ADIR chunk"); + if let Some(name) = &asset.name { + let bytes = name.as_bytes(); + let name_chunk = + ChunkDescriptor { id: FourCC(*b"NAME"), size: bytes.len() as u64, unk: 0, skip: 0 }; + file.write_le(&name_chunk)?; + file.write_all(bytes)?; + } + // Calculate size and rewrite FOOT header + form.size = file.stream_position()? - data_pos; + file.seek(SeekFrom::Start(form_pos))?; + file.write_le(&form)?; + file.flush()?; } Ok(()) } + +const K_CHUNK_AINF: FourCC = FourCC(*b"AINF"); +const K_CHUNK_META: FourCC = FourCC(*b"META"); +const K_CHUNK_NAME: FourCC = FourCC(*b"NAME"); + +fn package(args: PackageArgs) -> Result<()> { + let files = fs::read_dir(&args.input)?; + let mut package = Package { assets: vec![] }; + for result in files { + let entry = match result { + Ok(e) => e, + Err(e) => bail!("Failed to read directory entry: {:?}", e), + }; + + let path = entry.path(); + log::info!("Processing {}", path.display()); + let data = map_file(&path)?; + let (form, _, remain) = FormDescriptor::slice(&data, Endian::Little)?; + // log::info!("Found type {} version {}, {}", form.id, form.version, form.other_version); + let (foot, mut foot_data, _) = FormDescriptor::slice(remain, Endian::Little)?; + ensure!(foot.id == *b"FOOT"); + ensure!(foot.version == 1); + let mut aid: Option = None; + let mut meta: Option<&[u8]> = None; + let mut name: Option = None; + while !foot_data.is_empty() { + let (chunk, chunk_data, remain) = ChunkDescriptor::slice(foot_data, Endian::Little)?; + match chunk.id { + K_CHUNK_AINF => { + let ainf: AssetInfo = Cursor::new(chunk_data).read_type(Endian::Little)?; + // log::info!("AID: {}, compression: {}", ainf.id, ainf.compression_type); + aid = Some(ainf.id); + } + K_CHUNK_META => { + meta = Some(chunk_data); + } + K_CHUNK_NAME => { + name = Some(String::from_utf8(chunk_data.to_vec())?); + } + _ => {} + } + foot_data = remain; + } + let aid = match aid { + Some(a) => a, + None => bail!("Failed to locate asset ID"), + }; + package.assets.push(Asset { + id: aid, + kind: form.id, + name, + data: Cow::Owned(data[..data.len() - remain.len()].to_vec()), + meta: meta.map(|data| Cow::Owned(data.to_vec())), + info: AssetInfo { id: aid, compression_type: 0 /* TODO */ }, + version: form.version, + other_version: form.other_version, + }); + } + let mut file = + BufWriter::new(File::create(&args.output).with_context(|| { + format!("Failed to create output file '{}'", args.output.display()) + })?); + package.write(&mut file, Endian::Little)?; + file.flush()?; + Ok(()) +} diff --git a/src/cmd/txtr.rs b/src/cmd/txtr.rs new file mode 100644 index 0000000..066b055 --- /dev/null +++ b/src/cmd/txtr.rs @@ -0,0 +1,63 @@ +use std::path::PathBuf; + +use anyhow::{ensure, Result}; +use argh::FromArgs; +use binrw::Endian; + +use crate::{ + format::{chunk::ChunkDescriptor, rfrm::FormDescriptor, FourCC}, + util::file::map_file, +}; + +// Texture +pub const K_FORM_TXTR: FourCC = FourCC(*b"TXTR"); + +#[derive(FromArgs, PartialEq, Debug)] +/// process TXTR files +#[argh(subcommand, name = "txtr")] +pub struct Args { + #[argh(subcommand)] + command: SubCommand, +} + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand)] +enum SubCommand { + Convert(ConvertArgs), +} + +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// converts a TXTR file +#[argh(subcommand, name = "convert")] +pub struct ConvertArgs { + #[argh(positional)] + /// input file + input: PathBuf, + #[argh(positional)] + /// output directory + output: PathBuf, +} + +pub fn run(args: Args) -> Result<()> { + match args.command { + SubCommand::Convert(c_args) => convert(c_args), + } +} + +struct TextureHeader {} + +struct TextureMeta {} + +fn convert(args: ConvertArgs) -> Result<()> { + let mmap = map_file(args.input)?; + let (meta, meta_data, remain) = ChunkDescriptor::slice(&mmap, Endian::Little)?; + ensure!(meta.id == *b"META"); + + let (desc, data, _) = FormDescriptor::slice(remain, Endian::Little)?; + ensure!(desc.id == K_FORM_TXTR); + ensure!(desc.version == 47); + let (desc, head_data, remain) = ChunkDescriptor::slice(data, Endian::Little)?; + ensure!(desc.id == *b"HEAD"); + + todo!() +} diff --git a/src/format/adir.rs b/src/format/adir.rs new file mode 100644 index 0000000..3534635 --- /dev/null +++ b/src/format/adir.rs @@ -0,0 +1,30 @@ +use binrw::binrw; +use uuid::Uuid; + +use crate::format::FourCC; + +// Asset directory +pub const K_CHUNK_ADIR: FourCC = FourCC(*b"ADIR"); + +#[binrw] +#[derive(Clone, Debug, Default)] +pub struct AssetDirectory { + #[bw(try_calc = entries.len().try_into())] + pub entry_count: u32, + #[br(count = entry_count)] + pub entries: Vec, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct AssetDirectoryEntry { + pub asset_type: FourCC, + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + pub version: u32, + pub other_version: u32, + pub offset: u64, + pub decompressed_size: u64, + pub size: u64, +} diff --git a/src/format/chunk.rs b/src/format/chunk.rs new file mode 100644 index 0000000..47dfac5 --- /dev/null +++ b/src/format/chunk.rs @@ -0,0 +1,77 @@ +use std::io::{Read, Seek, SeekFrom, Write}; + +use anyhow::{anyhow, Result}; +use binrw::{binrw, io::Cursor, BinReaderExt, BinResult, BinWriterExt, Endian}; + +use crate::format::{ + adir::{AssetDirectory, K_CHUNK_ADIR}, + meta::{Metadata, K_CHUNK_META}, + strg::{StringTable, K_CHUNK_STRG}, + FourCC, +}; + +#[binrw] +#[derive(Clone, Debug)] +pub struct ChunkDescriptor { + pub id: FourCC, + pub size: u64, + pub unk: u32, + // game skips this amount of bytes before continuing + // but always 0? + pub skip: u64, +} + +pub const CHUNK_DESCRIPTOR_SIZE: usize = 24; + +impl ChunkDescriptor { + #[inline] + pub fn read(reader: &mut R, e: Endian) -> BinResult { + let desc: ChunkDescriptor = reader.read_type(e)?; + reader.seek(SeekFrom::Current(desc.skip as i64))?; + Ok(desc) + } + + #[inline] + pub fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { + let mut reader = Cursor::new(data); + let header = Self::read(&mut reader, e)?; + let start = reader.position(); + let slice = &data[start as usize..(start + header.size) as usize]; + let remain = &data[(start + header.size) as usize..]; + Ok((header, slice, remain)) + } + + pub fn write(&mut self, w: &mut W, e: Endian, mut cb: CB) -> Result<()> + where CB: FnMut(&mut W) -> Result<()> { + let form_pos = w.stream_position()?; + w.write_type(self, e)?; + let data_pos = w.stream_position()?; + cb(w)?; + let end_pos = w.stream_position()?; + w.seek(SeekFrom::Start(form_pos))?; + self.size = end_pos - data_pos; + w.write_type(self, e)?; + w.seek(SeekFrom::Start(end_pos))?; + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub enum ChunkType { + AssetDirectory(AssetDirectory), + Metadata(Metadata), + StringTable(StringTable), +} + +impl ChunkType { + #[inline] + pub fn read(data: &[u8], kind: FourCC, e: Endian) -> Result { + let mut reader = Cursor::new(data); + match kind { + K_CHUNK_ADIR => Ok(Self::AssetDirectory(reader.read_type(e)?)), + K_CHUNK_META => Ok(Self::Metadata(reader.read_type(e)?)), + K_CHUNK_STRG => Ok(Self::StringTable(reader.read_type(e)?)), + _ => Err(anyhow!("Unknown chunk type {:?}", kind)), + } + } +} diff --git a/src/format/meta.rs b/src/format/meta.rs new file mode 100644 index 0000000..503f840 --- /dev/null +++ b/src/format/meta.rs @@ -0,0 +1,25 @@ +use binrw::binrw; +use uuid::Uuid; + +use crate::format::FourCC; + +// Metadata +pub const K_CHUNK_META: FourCC = FourCC(*b"META"); + +#[binrw] +#[derive(Clone, Debug, Default)] +pub struct Metadata { + #[bw(try_calc = entries.len().try_into())] + pub entry_count: u32, + #[br(count = entry_count)] + pub entries: Vec, +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct MetadataEntry { + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + pub offset: u32, +} diff --git a/src/format/mod.rs b/src/format/mod.rs new file mode 100644 index 0000000..7fe9176 --- /dev/null +++ b/src/format/mod.rs @@ -0,0 +1,47 @@ +pub mod adir; +pub mod chunk; +pub mod meta; +pub mod rfrm; +pub mod strg; + +use std::fmt::{Debug, Display, Formatter, Write}; + +use binrw::binrw; + +use crate::array_ref; + +#[binrw] +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct FourCC(pub [u8; 4]); + +impl FourCC { + #[inline] + const fn swap(self) -> Self { Self([self.0[3], self.0[2], self.0[1], self.0[0]]) } +} + +impl Display for FourCC { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for c in self.0 { + f.write_char(c as char)?; + } + Ok(()) + } +} + +impl Debug for FourCC { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_char('"')?; + for c in self.0 { + f.write_char(c as char)?; + } + f.write_char('"')?; + Ok(()) + } +} + +impl PartialEq<[u8; 4]> for FourCC { + fn eq(&self, other: &[u8; 4]) -> bool { &self.0 == other } +} + +#[inline] +pub fn peek_four_cc(data: &[u8]) -> FourCC { FourCC(*array_ref!(data, 0, 4)) } diff --git a/src/format/rfrm.rs b/src/format/rfrm.rs new file mode 100644 index 0000000..eed41b7 --- /dev/null +++ b/src/format/rfrm.rs @@ -0,0 +1,55 @@ +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; + +use anyhow::Result; +use binrw::{binrw, BinReaderExt, BinResult, BinWriterExt, Endian}; + +use crate::format::FourCC; + +// Resource format +pub const K_CHUNK_RFRM: FourCC = FourCC(*b"RFRM"); +// Package file +pub const K_FORM_PAK: FourCC = FourCC(*b"PACK"); +// Table of contents +pub const K_FORM_TOC: FourCC = FourCC(*b"TOCC"); + +#[binrw] +#[brw(magic = b"RFRM")] +#[derive(Clone, Debug)] +pub struct FormDescriptor { + pub size: u64, + pub unk1: u64, + pub id: FourCC, + pub version: u32, + pub other_version: u32, +} + +impl FormDescriptor { + #[inline] + pub fn read(reader: &mut R, e: Endian) -> BinResult { + reader.read_type(e) + } + + #[inline] + pub fn slice(data: &[u8], e: Endian) -> BinResult<(Self, &[u8], &[u8])> { + let mut reader = Cursor::new(data); + let header = Self::read(&mut reader, e)?; + let start = reader.position(); + let slice = &data[start as usize..(start + header.size) as usize]; + let remain = &data[(start + header.size) as usize..]; + Ok((header, slice, remain)) + } + + pub fn write(&mut self, w: &mut W, e: Endian, mut cb: CB) -> Result<()> + where CB: FnMut(&mut W) -> Result<()> { + let form_pos = w.stream_position()?; + w.write_type(self, e)?; + let data_pos = w.stream_position()?; + cb(w)?; + let end_pos = w.stream_position()?; + w.seek(SeekFrom::Start(form_pos))?; + self.size = end_pos - data_pos; + w.write_type(self, e)?; + w.seek(SeekFrom::Start(end_pos))?; + Ok(()) + } +} diff --git a/src/format/strg.rs b/src/format/strg.rs new file mode 100644 index 0000000..4446cfa --- /dev/null +++ b/src/format/strg.rs @@ -0,0 +1,40 @@ +use binrw::binrw; +use uuid::Uuid; + +use crate::format::FourCC; + +// String table +pub const K_CHUNK_STRG: FourCC = FourCC(*b"STRG"); + +#[binrw] +#[derive(Clone, Debug, Default)] +pub struct StringTable { + #[bw(try_calc = entries.len().try_into())] + pub entry_count: u32, + #[br(count = entry_count)] + pub entries: Vec, +} + +impl StringTable { + pub fn name_for_uuid(&self, id: Uuid) -> Option { + self.entries + .iter() + .find(|e| e.asset_id == id) + .map(|e| String::from_utf8(e.name.clone()).unwrap()) + } +} + +#[binrw] +#[derive(Clone, Debug)] +pub struct StringTableEntry { + #[br(map = FourCC::swap)] + #[bw(map = |&f| f.swap())] + pub kind: FourCC, + #[br(map = Uuid::from_u128)] + #[bw(map = Uuid::as_u128)] + pub asset_id: Uuid, + #[bw(try_calc = name.len().try_into())] + pub name_length: u32, + #[br(count = name_length)] + pub name: Vec, +} diff --git a/src/main.rs b/src/main.rs index 82d7846..9faeb71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ +#![feature(cow_is_borrowed)] + mod argh_version; mod cmd; +mod format; mod util; use argh::FromArgs; +use cmd::SubCommand; #[derive(FromArgs, PartialEq, Debug)] /// GameCube/Wii decompilation project tools. @@ -11,12 +15,6 @@ struct TopLevel { command: SubCommand, } -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand)] -enum SubCommand { - Pak(cmd::pak::Args), -} - fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) .format_timestamp(None) @@ -27,6 +25,7 @@ fn main() { let args: TopLevel = argh_version::from_env(); let result = match args.command { SubCommand::Pak(args) => cmd::pak::run(args), + SubCommand::Txtr(args) => cmd::txtr::run(args), }; if let Err(e) = result { eprintln!("Failed: {e:?}"); diff --git a/src/util/mod.rs b/src/util/mod.rs index fc06744..9120437 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,14 @@ pub mod file; pub mod lzss; + +/// Creates a fixed-size array reference from a slice. +#[macro_export] +macro_rules! array_ref { + ($slice:expr, $offset:expr, $size:expr) => {{ + #[inline] + fn to_array(slice: &[T]) -> &[T; $size] { + unsafe { &*(slice.as_ptr() as *const [_; $size]) } + } + to_array(&$slice[$offset..$offset + $size]) + }}; +}