chore: initial commit
This commit is contained in:
commit
91ab6c26f7
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/Cargo.lock
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "ttmp"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
autoexamples = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
flate2 = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"] }
|
||||
sqpack = { git = "https://git.anna.lgbt/ascclemens/ttmp-rs", features = ["read"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.4"
|
||||
hex = "0.4"
|
||||
sha3 = "0.10"
|
||||
tempfile = "3"
|
||||
|
||||
[[bench]]
|
||||
name = "extract"
|
||||
harness = false
|
|
@ -0,0 +1,18 @@
|
|||
use std::io::Cursor;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use ttmp::ttmp_extractor::TtmpExtractor;
|
||||
|
||||
fn extract(c: &mut Criterion) {
|
||||
let ttmp2 = std::fs::read("schnapps.ttmp2").unwrap();
|
||||
c.bench_function("extract", |b| b.iter(|| {
|
||||
let mut extractor = TtmpExtractor::new(Cursor::new(&ttmp2)).unwrap();
|
||||
extractor.extract_all(|_| {
|
||||
Ok(Box::new(Cursor::new(Vec::new())))
|
||||
}).unwrap();
|
||||
}));
|
||||
}
|
||||
|
||||
criterion_group!(benches, extract);
|
||||
criterion_main!(benches);
|
|
@ -0,0 +1,344 @@
|
|||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs::File;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
|
||||
use ttmp::ttmp_extractor::TtmpExtractor;
|
||||
|
||||
pub fn main() {
|
||||
let arg = std::env::args().skip(1).next().unwrap();
|
||||
let file = File::open(&arg).unwrap();
|
||||
let mut extractor = TtmpExtractor::new(file).unwrap();
|
||||
extractor.extract_all(|file| {
|
||||
if file.file.full_path.contains("../") {
|
||||
return Err(std::io::Error::new(ErrorKind::Other, "tried to escape directory"));
|
||||
}
|
||||
|
||||
let group = file.group.map(|s| {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
s.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}).unwrap_or(0);
|
||||
let option = file.option.map(|s| {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
s.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}).unwrap_or(0);
|
||||
let path = Path::new(".")
|
||||
.join(format!("g{}", group))
|
||||
.join(format!("o{}", option))
|
||||
.join(&file.file.full_path);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
println!("extracting {}", path.to_string_lossy());
|
||||
Ok(Box::new(File::create(path)?))
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
// use std::fs::File;
|
||||
// use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
// use std::path::Path;
|
||||
//
|
||||
// use flate2::read::DeflateDecoder;
|
||||
// use sqpack::{DatBlockHeader, DatStdFileBlockInfos, FileKind, LodBlock, ModelBlock, SqPackFileInfoHeader};
|
||||
// use sqpack::binrw::{BinRead, BinWriterExt, VecArgs};
|
||||
// use sqpack::model::SqPackFileInfo;
|
||||
//
|
||||
// use ttmp::tracking_reader::TrackingReader;
|
||||
//
|
||||
// const MAX_LODS: usize = 3;
|
||||
//
|
||||
// pub fn main() {
|
||||
// let arg = std::env::args().skip(1).next().unwrap();
|
||||
// let file = File::open(&arg).unwrap();
|
||||
// let (manifest, mut zip) = ttmp::from_reader(file).unwrap();
|
||||
// let mut data_file = TrackingReader::new(zip.by_name("TTMPD.mpd").unwrap());
|
||||
//
|
||||
// let mut all_files = Vec::new();
|
||||
//
|
||||
// if let Some(pages) = &manifest.mod_pack_pages {
|
||||
// for page in pages {
|
||||
// for group in &page.mod_groups {
|
||||
// for option in &group.option_list {
|
||||
// for file in &option.mods_jsons {
|
||||
// all_files.push(file);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let Some(list) = &manifest.simple_mods_list {
|
||||
// all_files.extend(list);
|
||||
// }
|
||||
//
|
||||
// all_files.sort_unstable_by_key(|&file| file.mod_offset);
|
||||
//
|
||||
// let mut buf = [0; 4092];
|
||||
// for file in all_files {
|
||||
// data_file.read = 0;
|
||||
// let path = &file.full_path;
|
||||
// println!("extracting {}", path);
|
||||
//
|
||||
// if let Some(parent) = Path::new("./").join(Path::new(path)).parent() {
|
||||
// std::fs::create_dir_all(parent).unwrap();
|
||||
// }
|
||||
//
|
||||
// let expected = file.mod_size;
|
||||
// let mut file = File::create(path).unwrap();
|
||||
//
|
||||
// let info: SqPackFileInfoHeader = read_struct(&mut data_file, &mut buf);
|
||||
//
|
||||
// match info.kind {
|
||||
// FileKind::Empty => todo!(),
|
||||
// FileKind::Standard => {
|
||||
// let std_info: SqPackFileInfo = read_struct(&mut data_file, &mut buf);
|
||||
// let blocks: Vec<DatStdFileBlockInfos> = (0..std_info.number_of_blocks)
|
||||
// .map(|_| read_struct(&mut data_file, &mut buf))
|
||||
// .collect();
|
||||
//
|
||||
// let skip_amt = info.size as usize
|
||||
// - std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
// - std::mem::size_of::<SqPackFileInfo>()
|
||||
// - std::mem::size_of::<DatStdFileBlockInfos>() * std_info.number_of_blocks as usize;
|
||||
// skip(&mut data_file, &mut buf, skip_amt);
|
||||
//
|
||||
// for block in blocks {
|
||||
// read_block_into(&mut data_file, &mut file, &mut buf, block.compressed_size as usize);
|
||||
// }
|
||||
// }
|
||||
// FileKind::Model => {
|
||||
// let model_info: ModelBlock = read_struct(&mut data_file, &mut buf);
|
||||
// dbg!(&model_info);
|
||||
//
|
||||
// let block_counts = &model_info.block_num;
|
||||
// let total_blocks = block_counts.stack
|
||||
// + block_counts.runtime
|
||||
// + block_counts.vertex_buffer.iter().sum::<u16>()
|
||||
// + block_counts.edge_geometry_vertex_buffer.iter().sum::<u16>()
|
||||
// + block_counts.index_buffer.iter().sum::<u16>();
|
||||
// let block_sizes: Vec<u16> = read_vec(&mut data_file, total_blocks as usize);
|
||||
//
|
||||
// let skip_amt = info.size as usize
|
||||
// - std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
// - std::mem::size_of::<ModelBlock>()
|
||||
// - total_blocks as usize * std::mem::size_of::<u16>();
|
||||
// skip(&mut data_file, &mut buf, skip_amt);
|
||||
//
|
||||
// file.seek(SeekFrom::Start(0x44)).unwrap();
|
||||
//
|
||||
// let stack_size = read_blocks_into(
|
||||
// model_info.block_num.stack,
|
||||
// model_info.block_index.stack,
|
||||
// model_info.offset.stack,
|
||||
// &block_sizes,
|
||||
// &mut data_file,
|
||||
// &mut file,
|
||||
// &mut buf,
|
||||
// );
|
||||
//
|
||||
// let runtime_size = read_blocks_into(
|
||||
// model_info.block_num.runtime,
|
||||
// model_info.block_index.runtime,
|
||||
// model_info.offset.runtime,
|
||||
// &block_sizes,
|
||||
// &mut data_file,
|
||||
// &mut file,
|
||||
// &mut buf,
|
||||
// );
|
||||
//
|
||||
// let mut vertex_data_offsets = [0u32; MAX_LODS];
|
||||
// let mut vertex_buffer_sizes = [0u32; MAX_LODS];
|
||||
//
|
||||
// let mut index_data_offsets = [0u32; MAX_LODS];
|
||||
// let mut index_buffer_sizes = [0u32; MAX_LODS];
|
||||
//
|
||||
// for lod_index in 0..MAX_LODS {
|
||||
// // Vertex buffer
|
||||
// let block_count = model_info.block_num.vertex_buffer[lod_index];
|
||||
// if block_count != 0 {
|
||||
// if lod_index == 0 || block_count > 0 {
|
||||
// vertex_data_offsets[lod_index] = file.stream_position().unwrap().try_into().unwrap();
|
||||
// }
|
||||
//
|
||||
// vertex_buffer_sizes[lod_index] = read_blocks_into(
|
||||
// block_count,
|
||||
// model_info.block_index.vertex_buffer[lod_index],
|
||||
// // offset + model_info.offset.vertex_buffer[lod_index],
|
||||
// model_info.offset.vertex_buffer[lod_index],
|
||||
// &block_sizes,
|
||||
// &mut data_file,
|
||||
// &mut file,
|
||||
// &mut buf,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // Edge geometry vertex buffer
|
||||
// let block_count = model_info.block_num.edge_geometry_vertex_buffer[lod_index];
|
||||
// if block_count != 0 {
|
||||
// read_blocks_into(
|
||||
// block_count,
|
||||
// model_info.block_index.edge_geometry_vertex_buffer[lod_index],
|
||||
// // offset + model_info.offset.edge_geometry_vertex_buffer[lod_index],
|
||||
// model_info.offset.edge_geometry_vertex_buffer[lod_index],
|
||||
// &block_sizes,
|
||||
// &mut data_file,
|
||||
// &mut file,
|
||||
// &mut buf,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // Index buffer
|
||||
// let block_count = model_info.block_num.index_buffer[lod_index];
|
||||
// if block_count != 0 {
|
||||
// if lod_index == 0 || block_count > 0 {
|
||||
// index_data_offsets[lod_index] = file.stream_position().unwrap().try_into().unwrap();
|
||||
// }
|
||||
//
|
||||
// index_buffer_sizes[lod_index] = read_blocks_into(
|
||||
// block_count,
|
||||
// model_info.block_index.index_buffer[lod_index],
|
||||
// // offset + model_info.offset.index_buffer[lod_index],
|
||||
// model_info.offset.index_buffer[lod_index],
|
||||
// &block_sizes,
|
||||
// &mut data_file,
|
||||
// &mut file,
|
||||
// &mut buf,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Write out the header now we've collected the info for it.
|
||||
// file.seek(SeekFrom::Start(0)).unwrap();
|
||||
// file.write_le(&model_info.version).unwrap();
|
||||
// file.write_le(&stack_size).unwrap();
|
||||
// file.write_le(&runtime_size).unwrap();
|
||||
// file.write_le(&model_info.vertex_declaration_num).unwrap();
|
||||
// file.write_le(&model_info.material_num).unwrap();
|
||||
// file.write_le(&vertex_data_offsets).unwrap();
|
||||
// file.write_le(&index_data_offsets).unwrap();
|
||||
// file.write_le(&vertex_buffer_sizes).unwrap();
|
||||
// file.write_le(&index_buffer_sizes).unwrap();
|
||||
// file.write_le(&model_info.num_lods).unwrap();
|
||||
// file.write_le(&model_info.index_buffer_streaming_enabled).unwrap();
|
||||
// file.write_le(&model_info.edge_geometry_enabled).unwrap();
|
||||
// file.write_le(&0u8).unwrap();
|
||||
// }
|
||||
// FileKind::Texture => {
|
||||
// let std_info: SqPackFileInfo = read_struct(&mut data_file, &mut buf);
|
||||
// let blocks: Vec<LodBlock> = (0..std_info.number_of_blocks)
|
||||
// .map(|_| read_struct(&mut data_file, &mut buf))
|
||||
// .collect();
|
||||
//
|
||||
// let sub_block_count = blocks
|
||||
// .iter()
|
||||
// .fold(0, |acc, block| acc + block.block_count);
|
||||
// let sub_block_sizes: Vec<u16> = read_vec(&mut data_file, sub_block_count as usize);
|
||||
//
|
||||
// let skip_amt = info.size as usize
|
||||
// - std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
// - std::mem::size_of::<SqPackFileInfo>()
|
||||
// - std::mem::size_of::<LodBlock>() * std_info.number_of_blocks as usize
|
||||
// - std::mem::size_of::<u16>() * sub_block_sizes.len();
|
||||
// skip(&mut data_file, &mut buf, skip_amt);
|
||||
//
|
||||
// let mip_map_size = blocks[0].compressed_offset;
|
||||
// if mip_map_size > 0 {
|
||||
// let mut reader = (&mut data_file).take(mip_map_size as u64);
|
||||
// std::io::copy(&mut reader, &mut file).unwrap();
|
||||
// }
|
||||
//
|
||||
// let mut sub_block = 0;
|
||||
// for block in blocks {
|
||||
// for _ in 0..block.block_count {
|
||||
// read_block_into(&mut data_file, &mut file, &mut buf, sub_block_sizes[sub_block] as usize);
|
||||
// sub_block += 1;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if data_file.read < expected {
|
||||
// let to_skip = expected - data_file.read;
|
||||
// skip(&mut data_file, &mut buf, to_skip);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn read_block_into<R: Read, W: Write>(reader: &mut R, writer: &mut W, buf: &mut [u8], size: usize) -> u64 {
|
||||
// let header: DatBlockHeader = read_struct(reader, buf);
|
||||
//
|
||||
// let (read, actual) = if header.compressed_size == 32_000 {
|
||||
// // uncompressed
|
||||
// let mut reader = reader.take(header.uncompressed_size as u64);
|
||||
// let read = std::io::copy(&mut reader, writer).unwrap();
|
||||
// (read, read)
|
||||
// } else {
|
||||
// // compressed
|
||||
// let reader = reader.take(header.compressed_size as u64);
|
||||
// let mut decoder = DeflateDecoder::new(reader);
|
||||
// let read = std::io::copy(&mut decoder, writer).unwrap();
|
||||
// (header.compressed_size as u64, read)
|
||||
// };
|
||||
//
|
||||
// if (header.size as usize + read as usize) < size as usize {
|
||||
// let to_skip = size
|
||||
// - header.size as usize
|
||||
// - read as usize;
|
||||
// skip(reader, buf, to_skip);
|
||||
// }
|
||||
//
|
||||
// actual
|
||||
// }
|
||||
//
|
||||
// fn read_blocks_into(
|
||||
// block_count: u16,
|
||||
// block_index: u16,
|
||||
// _section_offset: u32,
|
||||
// block_sizes: &[u16],
|
||||
// reader: &mut impl Read,
|
||||
// writer: &mut impl Write,
|
||||
// buf: &mut [u8],
|
||||
// ) -> u32 {
|
||||
// let sizes = &block_sizes[block_index as usize..block_index as usize + block_count as usize];
|
||||
//
|
||||
// let mut total_read = 0u32;
|
||||
// for &size in sizes {
|
||||
// let bytes_read = read_block_into(reader, writer, buf, size as usize);
|
||||
// total_read += bytes_read as u32;
|
||||
// }
|
||||
//
|
||||
// total_read as u32
|
||||
// }
|
||||
//
|
||||
// fn read_struct<S: BinRead, R: Read>(reader: &mut R, buf: &mut [u8]) -> S
|
||||
// where S::Args: Default,
|
||||
// {
|
||||
// let size = std::mem::size_of::<S>();
|
||||
// reader.read_exact(&mut buf[..size]).unwrap();
|
||||
// S::read(&mut Cursor::new(&buf[..size])).unwrap()
|
||||
// }
|
||||
//
|
||||
// fn skip<R: Read>(reader: &mut R, buf: &mut [u8], size: usize) {
|
||||
// let mut left = size;
|
||||
// while left > 0 {
|
||||
// let to_read = std::cmp::min(left, buf.len());
|
||||
// left -= reader.read(&mut buf[..to_read]).unwrap();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn read_vec<S: BinRead<Args=()>, R: Read>(reader: &mut R, amount: usize) -> Vec<S> {
|
||||
// let mut buf = vec![0; amount * std::mem::size_of::<S>()];
|
||||
// reader.read_exact(&mut buf).unwrap();
|
||||
// <Vec<S>>::read_args(
|
||||
// &mut Cursor::new(buf),
|
||||
// VecArgs {
|
||||
// count: amount,
|
||||
// inner: (),
|
||||
// },
|
||||
// ).unwrap()
|
||||
// }
|
|
@ -0,0 +1,99 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use ttmp::ttmp_extractor::{ModFile, TtmpExtractor};
|
||||
|
||||
pub fn main() {
|
||||
let mut sha = Sha3_256::default();
|
||||
let arg = std::env::args().skip(1).next().unwrap();
|
||||
let file = File::open(&arg).unwrap();
|
||||
let extractor = TtmpExtractor::new(file).unwrap();
|
||||
let mut zip = extractor.zip().borrow_mut();
|
||||
let files = extractor.all_files_sorted();
|
||||
|
||||
let mut data_file = zip.by_name("TTMPD.mpd").unwrap();
|
||||
let version = &*extractor.manifest().version;
|
||||
|
||||
std::fs::create_dir_all("files").unwrap();
|
||||
|
||||
let mut hashes: HashMap<String, Vec<SavedFile>> = HashMap::with_capacity(files.len());
|
||||
let mut temp = tempfile::tempfile().unwrap();
|
||||
|
||||
for file in files {
|
||||
temp.set_len(0).unwrap();
|
||||
temp.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
// write each file into a temp file, then hash
|
||||
// mod files can get quite large, so storing them entirely in memory is probably a bad idea
|
||||
// let mut cursor = Cursor::new(Vec::with_capacity(file.file.mod_size));
|
||||
TtmpExtractor::extract_one_into(&file, &mut data_file, &mut temp).unwrap();
|
||||
// let data = cursor.into_inner();
|
||||
// sha.update(&data);
|
||||
temp.seek(SeekFrom::Start(0)).unwrap();
|
||||
std::io::copy(&mut temp, &mut sha).unwrap();
|
||||
temp.seek(SeekFrom::Start(0)).unwrap();
|
||||
let hash = sha.finalize_reset();
|
||||
let hash = hex::encode(&*hash);
|
||||
let new = !hashes.contains_key(&hash);
|
||||
let saved = SavedFile {
|
||||
author: extractor.manifest().author.clone(),
|
||||
package: extractor.manifest().name.clone(),
|
||||
package_version: extractor.manifest().version.clone(),
|
||||
game_path: file.file.full_path.clone(),
|
||||
group: file.group.map(ToOwned::to_owned),
|
||||
option: file.option.map(ToOwned::to_owned),
|
||||
};
|
||||
hashes.entry(hash.clone()).or_default().push(saved);
|
||||
|
||||
if new {
|
||||
let path = Path::new("files").join(&hash);
|
||||
std::io::copy(&mut temp, &mut File::create(&path).unwrap()).unwrap();
|
||||
// std::fs::write(&path, data).unwrap();
|
||||
println!("writing {}", path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
println!("{:#?}", hashes);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SavedFile {
|
||||
pub author: String,
|
||||
pub package: String,
|
||||
pub package_version: String,
|
||||
pub game_path: String,
|
||||
pub group: Option<String>,
|
||||
pub option: Option<String>,
|
||||
}
|
||||
|
||||
struct Multiplexer<W1, W2> {
|
||||
one: W1,
|
||||
two: W2,
|
||||
}
|
||||
|
||||
impl<W1, W2> Multiplexer<W1, W2> {
|
||||
fn new(one: W1, two: W2) -> Self {
|
||||
Self {
|
||||
one,
|
||||
two,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W1: Write, W2: Write> Write for Multiplexer<W1, W2> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let one = self.one.write(buf);
|
||||
let two = self.two.write(buf);
|
||||
one.and(two)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
let one = self.one.flush();
|
||||
let two = self.two.flush();
|
||||
one.and(two)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
use std::fs::File;
|
||||
|
||||
pub fn main() {
|
||||
let arg = std::env::args().skip(1).next().unwrap();
|
||||
let file = File::open(&arg).unwrap();
|
||||
let (manifest, _) = ttmp::from_reader(file).unwrap();
|
||||
println!("{:#?}", manifest);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
use thiserror::Error;
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("error processing mod zip file")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
#[error("io error reading/extracting mod")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid mod manifest")]
|
||||
InvalidManifest(#[from] serde_json::Error),
|
||||
#[error("the ttmp's data file was missing or corrupt")]
|
||||
MissingDataFile(zip::result::ZipError),
|
||||
#[error("the ttmp data file was corrupt")]
|
||||
SqPackError(#[from] sqpack::binrw::Error),
|
||||
#[error("error writing to output")]
|
||||
BinRwWrite(sqpack::binrw::Error),
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use std::io::{Read, Seek};
|
||||
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::model::ModPack;
|
||||
|
||||
pub mod model;
|
||||
pub mod error;
|
||||
pub(crate) mod tracking_reader;
|
||||
pub mod ttmp_extractor;
|
||||
|
||||
pub fn from_reader<R: Read + Seek>(reader: R) -> Result<(ModPack, ZipArchive<R>)> {
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::Zip)?;
|
||||
let manifest = zip.by_name("TTMPL.mpl").map_err(Error::Zip)?;
|
||||
let manifest = serde_json::from_reader(manifest).map_err(Error::InvalidManifest)?;
|
||||
Ok((manifest, zip))
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
pub use self::{
|
||||
mod_group::ModGroup,
|
||||
mod_option::ModOption,
|
||||
mod_pack::ModPack,
|
||||
mod_pack_page::ModPackPage,
|
||||
selection_type::SelectionType,
|
||||
simple_mod::SimpleMod,
|
||||
};
|
||||
|
||||
pub mod mod_group;
|
||||
pub mod mod_option;
|
||||
pub mod mod_pack;
|
||||
pub mod mod_pack_page;
|
||||
pub mod selection_type;
|
||||
pub mod simple_mod;
|
|
@ -0,0 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::{ModOption, SelectionType};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ModGroup {
|
||||
pub group_name: String,
|
||||
pub selection_type: SelectionType,
|
||||
pub option_list: Vec<ModOption>,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::{SelectionType, SimpleMod};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ModOption {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub image_path: Option<String>,
|
||||
pub mods_jsons: Vec<SimpleMod>,
|
||||
pub group_name: String,
|
||||
pub selection_type: SelectionType,
|
||||
pub is_checked: bool,
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::{ModPackPage, SimpleMod};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ModPack {
|
||||
pub minimum_framework_version: String,
|
||||
#[serde(rename = "TTMPVersion")]
|
||||
pub ttmp_version: String,
|
||||
pub name: String,
|
||||
pub author: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub url: String,
|
||||
pub mod_pack_pages: Option<Vec<ModPackPage>>,
|
||||
pub simple_mods_list: Option<Vec<SimpleMod>>,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::ModGroup;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct ModPackPage {
|
||||
pub page_index: i32,
|
||||
pub mod_groups: Vec<ModGroup>,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum SelectionType {
|
||||
Single,
|
||||
Multi,
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct SimpleMod {
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub full_path: String,
|
||||
pub dat_file: String,
|
||||
pub is_default: bool,
|
||||
pub mod_offset: usize,
|
||||
pub mod_size: usize,
|
||||
pub mod_pack_entry: Option<serde_json::Value>,
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
use std::io::Read;
|
||||
|
||||
pub struct TrackingReader<R> {
|
||||
inner: R,
|
||||
pub read: usize,
|
||||
}
|
||||
|
||||
impl<R: Read> TrackingReader<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
inner: reader,
|
||||
read: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for TrackingReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let res = self.inner.read(buf);
|
||||
if let Ok(read) = res {
|
||||
self.read += read;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
|
@ -0,0 +1,419 @@
|
|||
use std::cell::RefCell;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
|
||||
use flate2::read::DeflateDecoder;
|
||||
use sqpack::{DatBlockHeader, DatStdFileBlockInfos, FileKind, LodBlock, ModelBlock, SqPackFileInfo, SqPackFileInfoHeader};
|
||||
use sqpack::binrw::{BinRead, BinWriterExt, VecArgs};
|
||||
use sqpack::binrw::meta::ReadEndian;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{Error, ModPack};
|
||||
use crate::error::Result;
|
||||
use crate::model::SimpleMod;
|
||||
use crate::tracking_reader::TrackingReader;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub trait WriteSeek: Write + Seek {}
|
||||
|
||||
impl<T> WriteSeek for T
|
||||
where T: Write + Seek {}
|
||||
|
||||
pub struct TtmpExtractor<R> {
|
||||
manifest: ModPack,
|
||||
zip: RefCell<ZipArchive<R>>,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> TtmpExtractor<R> {
|
||||
pub fn new(reader: R) -> Result<Self> {
|
||||
let (manifest, zip) = crate::from_reader(reader)?;
|
||||
|
||||
Ok(Self {
|
||||
manifest,
|
||||
zip: RefCell::new(zip),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn manifest(&self) -> &ModPack {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
pub fn into_manifest(self) -> ModPack {
|
||||
self.manifest
|
||||
}
|
||||
|
||||
pub fn extract_all<F>(&mut self, writer_func: F) -> Result<()>
|
||||
where F: Fn(&ModFile) -> Result<Box<dyn WriteSeek>, std::io::Error> + 'static,
|
||||
{
|
||||
let all_files = self.all_files_sorted();
|
||||
|
||||
let mut zip = self.zip.borrow_mut();
|
||||
let mut data_file = TrackingReader::new(zip.by_name("TTMPD.mpd")
|
||||
.map_err(Error::MissingDataFile)?);
|
||||
|
||||
let mut buf = [0; 4096];
|
||||
for mod_file in all_files {
|
||||
let file = &*mod_file.file;
|
||||
data_file.read = 0;
|
||||
|
||||
// get the writer to write this file into
|
||||
let mut writer = writer_func(&mod_file)
|
||||
.map_err(Error::Io)?;
|
||||
let expected = file.mod_size;
|
||||
|
||||
let info: SqPackFileInfoHeader = Self::read_struct(&mut data_file, &mut buf)?;
|
||||
match info.kind {
|
||||
FileKind::Empty => todo!(),
|
||||
FileKind::Standard => {
|
||||
Self::extract_standard_file(&info, &mut data_file, &mut writer, &mut buf)?;
|
||||
}
|
||||
FileKind::Model => {
|
||||
Self::extract_model_file(&info, &mut data_file, &mut writer, &mut buf)?;
|
||||
}
|
||||
FileKind::Texture => {
|
||||
Self::extract_texture_file(&info, &mut data_file, &mut writer, &mut buf)?;
|
||||
}
|
||||
}
|
||||
|
||||
if data_file.read < expected {
|
||||
let to_skip = expected - data_file.read;
|
||||
Self::skip(&mut data_file, &mut buf, to_skip)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn zip(&self) -> &RefCell<ZipArchive<R>> {
|
||||
&self.zip
|
||||
}
|
||||
|
||||
pub fn all_files_sorted(&self) -> Vec<ModFile> {
|
||||
let mut all_files = Vec::new();
|
||||
|
||||
if let Some(pages) = &self.manifest.mod_pack_pages {
|
||||
for page in pages {
|
||||
for group in &page.mod_groups {
|
||||
for option in &group.option_list {
|
||||
for file in &option.mods_jsons {
|
||||
all_files.push(ModFile {
|
||||
group: Some(&*option.group_name),
|
||||
option: Some(&*option.name),
|
||||
file,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(list) = &self.manifest.simple_mods_list {
|
||||
all_files.extend(list.iter().map(|file| ModFile {
|
||||
group: None,
|
||||
option: None,
|
||||
file,
|
||||
}));
|
||||
}
|
||||
|
||||
all_files.sort_unstable_by_key(|file| file.file.mod_offset);
|
||||
all_files
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> TtmpExtractor<R> {
|
||||
pub fn extract_one_into<W: Write + Seek>(mod_file: &ModFile, mut reader: R, mut writer: W) -> Result<()> {
|
||||
let mut reader = TrackingReader::new(&mut reader);
|
||||
let mut buf = [0; 4096];
|
||||
let file = &*mod_file.file;
|
||||
let expected = file.mod_size;
|
||||
|
||||
let info: SqPackFileInfoHeader = Self::read_struct(&mut reader, &mut buf)?;
|
||||
match info.kind {
|
||||
FileKind::Empty => todo!(),
|
||||
FileKind::Standard => {
|
||||
Self::extract_standard_file(&info, &mut reader, &mut writer, &mut buf)?;
|
||||
}
|
||||
FileKind::Model => {
|
||||
Self::extract_model_file(&info, &mut reader, &mut writer, &mut buf)?;
|
||||
}
|
||||
FileKind::Texture => {
|
||||
Self::extract_texture_file(&info, &mut reader, &mut writer, &mut buf)?;
|
||||
}
|
||||
}
|
||||
|
||||
if reader.read < expected {
|
||||
let to_skip = expected - reader.read;
|
||||
Self::skip(&mut reader, &mut buf, to_skip)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_standard_file<T: Read, W: Write>(info: &SqPackFileInfoHeader, mut data_file: T, mut writer: W, buf: &mut [u8]) -> Result<()> {
|
||||
let std_info: SqPackFileInfo = Self::read_struct(&mut data_file, buf)?;
|
||||
let blocks: Vec<DatStdFileBlockInfos> = (0..std_info.number_of_blocks)
|
||||
.map(|_| Self::read_struct(&mut data_file, buf))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let skip_amt = info.size as usize
|
||||
- std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
- std::mem::size_of::<SqPackFileInfo>()
|
||||
- std::mem::size_of::<DatStdFileBlockInfos>() * std_info.number_of_blocks as usize;
|
||||
Self::skip(&mut data_file, buf, skip_amt)?;
|
||||
|
||||
for block in blocks {
|
||||
Self::read_block_into(&mut data_file, &mut writer, buf, block.compressed_size as usize)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_model_file<T: Read, W: Write + Seek>(info: &SqPackFileInfoHeader, mut reader: T, mut writer: W, buf: &mut [u8]) -> Result<()> {
|
||||
let model_info: ModelBlock = Self::read_struct(&mut reader, buf)?;
|
||||
|
||||
let block_counts = &model_info.block_num;
|
||||
let total_blocks = block_counts.stack
|
||||
+ block_counts.runtime
|
||||
+ block_counts.vertex_buffer.iter().sum::<u16>()
|
||||
+ block_counts.edge_geometry_vertex_buffer.iter().sum::<u16>()
|
||||
+ block_counts.index_buffer.iter().sum::<u16>();
|
||||
let block_sizes: Vec<u16> = Self::read_vec(&mut reader, total_blocks as usize, buf)?;
|
||||
|
||||
let skip_amt = info.size as usize
|
||||
- std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
- std::mem::size_of::<ModelBlock>()
|
||||
- std::mem::size_of::<u16>() * total_blocks as usize;
|
||||
Self::skip(&mut reader, buf, skip_amt)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(0x44)).map_err(Error::Io)?;
|
||||
|
||||
let stack_size = Self::read_blocks_into(
|
||||
model_info.block_num.stack,
|
||||
model_info.block_index.stack,
|
||||
model_info.offset.stack,
|
||||
&block_sizes,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
buf,
|
||||
)?;
|
||||
|
||||
let runtime_size = Self::read_blocks_into(
|
||||
model_info.block_num.runtime,
|
||||
model_info.block_index.runtime,
|
||||
model_info.offset.runtime,
|
||||
&block_sizes,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
buf,
|
||||
)?;
|
||||
|
||||
const MAX_LODS: usize = 3;
|
||||
let mut vertex_data_offsets = [0u32; MAX_LODS];
|
||||
let mut vertex_buffer_sizes = [0u32; MAX_LODS];
|
||||
|
||||
let mut index_data_offsets = [0u32; MAX_LODS];
|
||||
let mut index_buffer_sizes = [0u32; MAX_LODS];
|
||||
|
||||
for lod_index in 0..MAX_LODS {
|
||||
// Vertex buffer
|
||||
let block_count = model_info.block_num.vertex_buffer[lod_index];
|
||||
if block_count != 0 {
|
||||
if lod_index == 0 || block_count > 0 {
|
||||
vertex_data_offsets[lod_index] = writer.stream_position().map_err(Error::Io)? as u32;
|
||||
}
|
||||
|
||||
vertex_buffer_sizes[lod_index] = Self::read_blocks_into(
|
||||
block_count,
|
||||
model_info.block_index.vertex_buffer[lod_index],
|
||||
model_info.offset.vertex_buffer[lod_index],
|
||||
&block_sizes,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
buf,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Edge geometry vertex buffer
|
||||
let block_count = model_info.block_num.edge_geometry_vertex_buffer[lod_index];
|
||||
if block_count != 0 {
|
||||
Self::read_blocks_into(
|
||||
block_count,
|
||||
model_info.block_index.edge_geometry_vertex_buffer[lod_index],
|
||||
model_info.offset.edge_geometry_vertex_buffer[lod_index],
|
||||
&block_sizes,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
buf,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Index buffer
|
||||
let block_count = model_info.block_num.index_buffer[lod_index];
|
||||
if block_count != 0 {
|
||||
if lod_index == 0 || block_count > 0 {
|
||||
index_data_offsets[lod_index] = writer.stream_position().map_err(Error::Io)? as u32;
|
||||
}
|
||||
|
||||
index_buffer_sizes[lod_index] = Self::read_blocks_into(
|
||||
block_count,
|
||||
model_info.block_index.index_buffer[lod_index],
|
||||
model_info.offset.index_buffer[lod_index],
|
||||
&block_sizes,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
buf,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the header now we've collected the info for it.
|
||||
writer.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
|
||||
writer.write_le(&model_info.version).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&stack_size).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&runtime_size).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&model_info.vertex_declaration_num).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&model_info.material_num).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&vertex_data_offsets).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&index_data_offsets).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&vertex_buffer_sizes).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&index_buffer_sizes).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&model_info.num_lods).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&model_info.index_buffer_streaming_enabled).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&model_info.edge_geometry_enabled).map_err(Error::BinRwWrite)?;
|
||||
writer.write_le(&0u8).map_err(Error::BinRwWrite)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_texture_file<T: Read, W: Write>(info: &SqPackFileInfoHeader, mut reader: T, mut writer: W, buf: &mut [u8]) -> Result<()> {
|
||||
let std_info: SqPackFileInfo = Self::read_struct(&mut reader, buf)?;
|
||||
let blocks: Vec<LodBlock> = (0..std_info.number_of_blocks)
|
||||
.map(|_| Self::read_struct(&mut reader, buf))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let sub_block_count = blocks
|
||||
.iter()
|
||||
.fold(0, |acc, block| acc + block.block_count);
|
||||
let sub_block_sizes: Vec<u16> = Self::read_vec(&mut reader, sub_block_count as usize, buf)?;
|
||||
|
||||
let skip_amt = info.size as usize
|
||||
- std::mem::size_of::<SqPackFileInfoHeader>()
|
||||
- std::mem::size_of::<SqPackFileInfo>()
|
||||
- std::mem::size_of::<LodBlock>() * std_info.number_of_blocks as usize
|
||||
- std::mem::size_of::<u16>() * sub_block_sizes.len();
|
||||
Self::skip(&mut reader, buf, skip_amt)?;
|
||||
|
||||
let mip_map_size = blocks[0].compressed_offset;
|
||||
if mip_map_size > 0 {
|
||||
let mut reader = (&mut reader).take(mip_map_size as u64);
|
||||
std::io::copy(&mut reader, &mut writer).map_err(Error::Io)?;
|
||||
}
|
||||
|
||||
let mut sub_block = 0;
|
||||
for block in blocks {
|
||||
for _ in 0..block.block_count {
|
||||
Self::read_block_into(&mut reader, &mut writer, buf, sub_block_sizes[sub_block] as usize)?;
|
||||
sub_block += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_struct<S: BinRead + ReadEndian, T: Read>(mut reader: T, buf: &mut [u8]) -> Result<S>
|
||||
where S::Args: Default,
|
||||
{
|
||||
let size = std::mem::size_of::<S>();
|
||||
if size > buf.len() {
|
||||
panic!();
|
||||
}
|
||||
|
||||
reader.read_exact(&mut buf[..size]).map_err(Error::Io)?;
|
||||
S::read(&mut Cursor::new(&buf[..size]))
|
||||
.map_err(Error::SqPackError)
|
||||
}
|
||||
|
||||
fn read_block_into<T: Read, W: Write>(mut reader: T, mut writer: W, buf: &mut [u8], size: usize) -> Result<u64> {
|
||||
let header: DatBlockHeader = Self::read_struct(&mut reader, buf)?;
|
||||
|
||||
let (read, actual) = if header.compressed_size == 32_000 {
|
||||
// uncompressed
|
||||
let mut reader = (&mut reader).take(header.uncompressed_size as u64);
|
||||
let read = std::io::copy(&mut reader, &mut writer).map_err(Error::Io)?;
|
||||
(read, read)
|
||||
} else {
|
||||
// compressed
|
||||
let reader = (&mut reader).take(header.compressed_size as u64);
|
||||
let mut decoder = DeflateDecoder::new(reader);
|
||||
let read = std::io::copy(&mut decoder, &mut writer).map_err(Error::Io)?;
|
||||
(header.compressed_size as u64, read)
|
||||
};
|
||||
|
||||
if (header.size as usize + read as usize) < size as usize {
|
||||
let to_skip = size
|
||||
- header.size as usize
|
||||
- read as usize;
|
||||
Self::skip(&mut reader, buf, to_skip)?;
|
||||
}
|
||||
|
||||
Ok(actual)
|
||||
}
|
||||
|
||||
fn read_blocks_into<T: Read, W: Write>(
|
||||
block_count: u16,
|
||||
block_index: u16,
|
||||
_section_offset: u32,
|
||||
block_sizes: &[u16],
|
||||
mut reader: T,
|
||||
mut writer: W,
|
||||
buf: &mut [u8],
|
||||
) -> Result<u32> {
|
||||
let sizes = &block_sizes[block_index as usize..block_index as usize + block_count as usize];
|
||||
sizes
|
||||
.iter()
|
||||
.try_fold(0, |acc, &size| {
|
||||
let bytes_read = Self::read_block_into(&mut reader, &mut writer, buf, size as usize)?;
|
||||
Ok(acc + bytes_read as u32)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_vec<S: BinRead<Args=()>, T: Read>(mut reader: T, amount: usize, buf: &mut [u8]) -> Result<Vec<S>> {
|
||||
let size_needed = amount * std::mem::size_of::<S>();
|
||||
if size_needed <= buf.len() {
|
||||
reader.read_exact(&mut buf[..size_needed]).map_err(Error::Io)?;
|
||||
<Vec<S>>::read_le_args(
|
||||
&mut Cursor::new(&buf[..size_needed]),
|
||||
VecArgs {
|
||||
count: amount,
|
||||
inner: (),
|
||||
},
|
||||
).map_err(Error::SqPackError)
|
||||
} else {
|
||||
let mut buf = vec![0; size_needed];
|
||||
reader.read_exact(&mut buf).map_err(Error::Io)?;
|
||||
<Vec<S>>::read_le_args(
|
||||
&mut Cursor::new(buf),
|
||||
VecArgs {
|
||||
count: amount,
|
||||
inner: (),
|
||||
},
|
||||
).map_err(Error::SqPackError)
|
||||
}
|
||||
}
|
||||
|
||||
fn skip<T: Read>(mut reader: T, buf: &mut [u8], amt: usize) -> Result<()> {
|
||||
let mut left = amt;
|
||||
while left > 0 {
|
||||
let to_read = std::cmp::min(left, buf.len());
|
||||
left -= reader.read(&mut buf[..to_read]).map_err(Error::Io)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ModFile<'a> {
|
||||
pub group: Option<&'a str>,
|
||||
pub option: Option<&'a str>,
|
||||
pub file: &'a SimpleMod,
|
||||
}
|
Loading…
Reference in New Issue