v0.2.0 refactoring

This commit is contained in:
2025-07-28 14:51:55 +08:00
parent 601e217276
commit 6e95351fa0
4 changed files with 1108 additions and 155 deletions

711
Cargo.lock generated

File diff suppressed because it is too large Load Diff

264
src/app.rs Normal file
View File

@@ -0,0 +1,264 @@
use std::{
io::{self, Write},
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use fs_err as fs;
use color_eyre::eyre::bail;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use fs::OpenOptions;
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
use parking_lot::Mutex;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use re_tex::tex::Tex;
use ree_pak_core::{
filename::{FileNameExt, FileNameTable},
pak::PakEntry,
read::archive::PakArchiveReader,
write::FileOptions,
};
const FILE_NAME_LIST: &[u8] = include_bytes!("../assets/MHWs_STM_Release.list.zst");
#[derive(Debug, Clone)]
pub struct Config {
pub input_path: Option<String>,
pub output_path: Option<String>,
pub use_full_package_mode: bool,
pub use_feature_clone: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
input_path: Default::default(),
output_path: Default::default(),
use_full_package_mode: false,
use_feature_clone: true,
}
}
}
#[derive(Default)]
pub struct App {
config: Config,
}
impl App {
pub fn run(&mut self) -> color_eyre::Result<()> {
println!("Version v{} - Tool by @Eigeen", env!("CARGO_PKG_VERSION"));
println!("Get updates at https://github.com/eigeen/mhws-tex-decompressor");
println!();
// Mode selection
let mode = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select mode")
.items(&["Automatic", "Manual"])
.default(0)
.interact()?;
let auto_mode = mode == 0;
if auto_mode {
self.auto_mode()
} else {
self.manual_mode()
}
}
fn auto_mode(&mut self) -> color_eyre::Result<()> {
todo!()
}
fn manual_mode(&mut self) -> color_eyre::Result<()> {
let input: String = Input::with_theme(&ColorfulTheme::default())
.show_default(true)
.default("re_chunk_000.pak.sub_000.pak".to_string())
.with_prompt("Input .pak file path")
.interact_text()
.unwrap()
.trim_matches(|c| c == '\"' || c == '\'')
.to_string();
let input_path = Path::new(&input);
if !input_path.is_file() {
bail!("input file not exists.");
}
const FALSE_TRUE_SELECTION: [&str; 2] = ["False", "True"];
let use_full_package_mode = Select::with_theme(&ColorfulTheme::default())
.with_prompt(
"Package all files, including non-tex files (for replacing original files)",
)
.default(0)
.items(&FALSE_TRUE_SELECTION)
.interact()
.unwrap();
let use_full_package_mode = use_full_package_mode == 1;
let use_feature_clone = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Clone feature flags from original file?")
.default(1)
.items(&FALSE_TRUE_SELECTION)
.interact()
.unwrap();
let use_feature_clone = use_feature_clone == 1;
println!("Loading embedded file name table...");
let filename_table = FileNameTable::from_bytes(FILE_NAME_LIST)?;
let file = fs::File::open(input_path)?;
let mut reader = io::BufReader::new(file);
println!("Reading pak archive...");
let pak_archive = ree_pak_core::read::read_archive(&mut reader)?;
let archive_reader = PakArchiveReader::new(reader, &pak_archive);
let archive_reader_mtx = Mutex::new(archive_reader);
// filtered entries
let entries = if use_full_package_mode {
pak_archive.entries().iter().collect::<Vec<_>>()
} else {
println!("Filtering entries...");
pak_archive
.entries()
.iter()
.filter(|entry| is_tex_file(entry.hash(), &filename_table))
.collect::<Vec<_>>()
};
// new pak archive
let output_path = input_path.with_extension("uncompressed.pak");
println!("Output file: {}", output_path.to_string_lossy());
let out_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(output_path)?;
let pak_writer = ree_pak_core::write::PakWriter::new(out_file, entries.len() as u64);
let pak_writer_mtx = Arc::new(Mutex::new(pak_writer));
let bar = ProgressBar::new(entries.len() as u64);
bar.set_style(
ProgressStyle::default_bar()
.template("Bytes written: {msg}\n{pos}/{len} {wide_bar}")?,
);
bar.enable_steady_tick(Duration::from_millis(200));
let pak_writer_mtx1 = Arc::clone(&pak_writer_mtx);
let bar1 = bar.clone();
let bytes_written = AtomicUsize::new(0);
let err = entries
.par_iter()
.try_for_each(move |&entry| -> color_eyre::Result<()> {
let pak_writer_mtx = &pak_writer_mtx1;
let bar = &bar1;
// read raw tex file
// parse tex file
let mut entry_reader = {
let mut archive_reader = archive_reader_mtx.lock();
archive_reader.owned_entry_reader(entry.clone())?
};
if !is_tex_file(entry.hash(), &filename_table) {
// plain file, just copy
let mut buf = vec![];
std::io::copy(&mut entry_reader, &mut buf)?;
let mut pak_writer = pak_writer_mtx.lock();
let write_bytes = write_to_pak(
&mut pak_writer,
entry,
entry.hash(),
&buf,
use_feature_clone,
)?;
bytes_written.fetch_add(write_bytes, Ordering::SeqCst);
} else {
let mut tex = Tex::from_reader(&mut entry_reader)?;
// decompress mipmaps
tex.batch_decompress()?;
let tex_bytes = tex.as_bytes()?;
let mut pak_writer = pak_writer_mtx.lock();
let write_bytes = write_to_pak(
&mut pak_writer,
entry,
entry.hash(),
&tex_bytes,
use_feature_clone,
)?;
bytes_written.fetch_add(write_bytes, Ordering::SeqCst);
}
bar.inc(1);
if bar.position() % 100 == 0 {
bar.set_message(
HumanBytes(bytes_written.load(Ordering::SeqCst) as u64).to_string(),
);
}
Ok(())
});
if let Err(e) = err {
eprintln!("Error occurred when processing tex: {e}");
eprintln!(
"The process terminated early, we'll save the current processed tex files to pak file."
);
}
match Arc::try_unwrap(pak_writer_mtx) {
Ok(pak_writer) => pak_writer.into_inner().finish()?,
Err(_) => panic!("Arc::try_unwrap failed"),
};
bar.finish();
println!("{}", "Done!".cyan().bold());
if !use_full_package_mode {
println!(
"You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`."
);
}
Ok(())
}
}
fn is_tex_file(hash: u64, file_name_table: &FileNameTable) -> bool {
let Some(file_name) = file_name_table.get_file_name(hash) else {
return false;
};
file_name.get_name().ends_with(".tex.241106027")
}
fn write_to_pak<W>(
writer: &mut ree_pak_core::write::PakWriter<W>,
entry: &PakEntry,
file_name: impl FileNameExt,
data: &[u8],
use_feature_clone: bool,
) -> color_eyre::Result<usize>
where
W: io::Write + io::Seek,
{
let mut file_options = FileOptions::default();
if use_feature_clone {
file_options = file_options.with_unk_attr(*entry.unk_attr())
}
writer.start_file(file_name, file_options)?;
writer.write_all(data)?;
Ok(data.len())
}
fn wait_for_exit() {
let _: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Press Enter to exit")
.allow_empty(true)
.interact_text()
.unwrap();
}

5
src/chunk.rs Normal file
View File

@@ -0,0 +1,5 @@
//! Chunk file name format
pub struct ChunkName {
major_id: u32,
}

View File

@@ -1,3 +1,6 @@
mod app;
mod chunk;
use std::{ use std::{
io::{self, Write}, io::{self, Write},
path::Path, path::Path,
@@ -25,19 +28,27 @@ use ree_pak_core::{
write::FileOptions, write::FileOptions,
}; };
const FILE_NAME_LIST: &[u8] = include_bytes!("../assets/MHWs_STM_Release.list.zst");
fn main() { fn main() {
std::panic::set_hook(Box::new(panic_hook)); std::panic::set_hook(Box::new(panic_hook));
println!("Version v{} - Tool by @Eigeen", env!("CARGO_PKG_VERSION")); let mut app = app::App::default();
if let Err(e) = app.run() {
if let Err(e) = main_entry() {
eprintln!("{}: {:#}", "Error".red().bold(), e); eprintln!("{}: {:#}", "Error".red().bold(), e);
wait_for_exit(); wait_for_exit();
std::process::exit(1); std::process::exit(1);
} }
wait_for_exit(); wait_for_exit();
// println!("Version v{} - Tool by @Eigeen", env!("CARGO_PKG_VERSION"));
// println!("Get updates at https://github.com/eigeen/mhws-tex-decompressor");
// println!();
// if let Err(e) = main_entry() {
// eprintln!("{}: {:#}", "Error".red().bold(), e);
// wait_for_exit();
// std::process::exit(1);
// }
// wait_for_exit();
} }
fn panic_hook(info: &std::panic::PanicHookInfo) { fn panic_hook(info: &std::panic::PanicHookInfo) {
@@ -46,154 +57,154 @@ fn panic_hook(info: &std::panic::PanicHookInfo) {
std::process::exit(1); std::process::exit(1);
} }
fn main_entry() -> color_eyre::Result<()> { // fn main_entry() -> color_eyre::Result<()> {
let input: String = Input::with_theme(&ColorfulTheme::default()) // let input: String = Input::with_theme(&ColorfulTheme::default())
.show_default(true) // .show_default(true)
.default("re_chunk_000.pak.sub_000.pak".to_string()) // .default("re_chunk_000.pak.sub_000.pak".to_string())
.with_prompt("Input .pak file path") // .with_prompt("Input .pak file path")
.interact_text() // .interact_text()
.unwrap() // .unwrap()
.trim_matches(|c| c == '\"' || c == '\'') // .trim_matches(|c| c == '\"' || c == '\'')
.to_string(); // .to_string();
let input_path = Path::new(&input); // let input_path = Path::new(&input);
if !input_path.is_file() { // if !input_path.is_file() {
bail!("input file not exists."); // bail!("input file not exists.");
} // }
const FALSE_TRUE_SELECTION: [&str; 2] = ["False", "True"]; // const FALSE_TRUE_SELECTION: [&str; 2] = ["False", "True"];
let use_full_package_mode = Select::with_theme(&ColorfulTheme::default()) // let use_full_package_mode = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Package all files, including non-tex files (for replacing original files)") // .with_prompt("Package all files, including non-tex files (for replacing original files)")
.default(0) // .default(0)
.items(&FALSE_TRUE_SELECTION) // .items(&FALSE_TRUE_SELECTION)
.interact() // .interact()
.unwrap(); // .unwrap();
let use_full_package_mode = use_full_package_mode == 1; // let use_full_package_mode = use_full_package_mode == 1;
let use_feature_clone = Select::with_theme(&ColorfulTheme::default()) // let use_feature_clone = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Clone feature flags from original file?") // .with_prompt("Clone feature flags from original file?")
.default(1) // .default(1)
.items(&FALSE_TRUE_SELECTION) // .items(&FALSE_TRUE_SELECTION)
.interact() // .interact()
.unwrap(); // .unwrap();
let use_feature_clone = use_feature_clone == 1; // let use_feature_clone = use_feature_clone == 1;
println!("Loading embedded file name table..."); // println!("Loading embedded file name table...");
let filename_table = FileNameTable::from_bytes(FILE_NAME_LIST)?; // let filename_table = FileNameTable::from_bytes(FILE_NAME_LIST)?;
let file = fs::File::open(input_path)?; // let file = fs::File::open(input_path)?;
let mut reader = io::BufReader::new(file); // let mut reader = io::BufReader::new(file);
println!("Reading pak archive..."); // println!("Reading pak archive...");
let pak_archive = ree_pak_core::read::read_archive(&mut reader)?; // let pak_archive = ree_pak_core::read::read_archive(&mut reader)?;
let archive_reader = PakArchiveReader::new(reader, &pak_archive); // let archive_reader = PakArchiveReader::new(reader, &pak_archive);
let archive_reader_mtx = Mutex::new(archive_reader); // let archive_reader_mtx = Mutex::new(archive_reader);
// filtered entries // // filtered entries
let entries = if use_full_package_mode { // let entries = if use_full_package_mode {
pak_archive.entries().iter().collect::<Vec<_>>() // pak_archive.entries().iter().collect::<Vec<_>>()
} else { // } else {
println!("Filtering entries..."); // println!("Filtering entries...");
pak_archive // pak_archive
.entries() // .entries()
.iter() // .iter()
.filter(|entry| is_tex_file(entry.hash(), &filename_table)) // .filter(|entry| is_tex_file(entry.hash(), &filename_table))
.collect::<Vec<_>>() // .collect::<Vec<_>>()
}; // };
// new pak archive // // new pak archive
let output_path = input_path.with_extension("uncompressed.pak"); // let output_path = input_path.with_extension("uncompressed.pak");
println!("Output file: {}", output_path.to_string_lossy()); // println!("Output file: {}", output_path.to_string_lossy());
let out_file = OpenOptions::new() // let out_file = OpenOptions::new()
.create(true) // .create(true)
.truncate(true) // .truncate(true)
.write(true) // .write(true)
.open(output_path)?; // .open(output_path)?;
let pak_writer = ree_pak_core::write::PakWriter::new(out_file, entries.len() as u64); // let pak_writer = ree_pak_core::write::PakWriter::new(out_file, entries.len() as u64);
let pak_writer_mtx = Arc::new(Mutex::new(pak_writer)); // let pak_writer_mtx = Arc::new(Mutex::new(pak_writer));
let bar = ProgressBar::new(entries.len() as u64); // let bar = ProgressBar::new(entries.len() as u64);
bar.set_style( // bar.set_style(
ProgressStyle::default_bar().template("Bytes written: {msg}\n{pos}/{len} {wide_bar}")?, // ProgressStyle::default_bar().template("Bytes written: {msg}\n{pos}/{len} {wide_bar}")?,
); // );
bar.enable_steady_tick(Duration::from_millis(200)); // bar.enable_steady_tick(Duration::from_millis(200));
let pak_writer_mtx1 = Arc::clone(&pak_writer_mtx); // let pak_writer_mtx1 = Arc::clone(&pak_writer_mtx);
let bar1 = bar.clone(); // let bar1 = bar.clone();
let bytes_written = AtomicUsize::new(0); // let bytes_written = AtomicUsize::new(0);
let err = entries // let err = entries
.par_iter() // .par_iter()
.try_for_each(move |&entry| -> color_eyre::Result<()> { // .try_for_each(move |&entry| -> color_eyre::Result<()> {
let pak_writer_mtx = &pak_writer_mtx1; // let pak_writer_mtx = &pak_writer_mtx1;
let bar = &bar1; // let bar = &bar1;
// read raw tex file // // read raw tex file
// parse tex file // // parse tex file
let mut entry_reader = { // let mut entry_reader = {
let mut archive_reader = archive_reader_mtx.lock(); // let mut archive_reader = archive_reader_mtx.lock();
archive_reader.owned_entry_reader(entry.clone())? // archive_reader.owned_entry_reader(entry.clone())?
}; // };
if !is_tex_file(entry.hash(), &filename_table) { // if !is_tex_file(entry.hash(), &filename_table) {
// plain file, just copy // // plain file, just copy
let mut buf = vec![]; // let mut buf = vec![];
std::io::copy(&mut entry_reader, &mut buf)?; // std::io::copy(&mut entry_reader, &mut buf)?;
let mut pak_writer = pak_writer_mtx.lock(); // let mut pak_writer = pak_writer_mtx.lock();
let write_bytes = write_to_pak( // let write_bytes = write_to_pak(
&mut pak_writer, // &mut pak_writer,
entry, // entry,
entry.hash(), // entry.hash(),
&buf, // &buf,
use_feature_clone, // use_feature_clone,
)?; // )?;
bytes_written.fetch_add(write_bytes, Ordering::SeqCst); // bytes_written.fetch_add(write_bytes, Ordering::SeqCst);
} else { // } else {
let mut tex = Tex::from_reader(&mut entry_reader)?; // let mut tex = Tex::from_reader(&mut entry_reader)?;
// decompress mipmaps // // decompress mipmaps
tex.batch_decompress()?; // tex.batch_decompress()?;
let tex_bytes = tex.as_bytes()?; // let tex_bytes = tex.as_bytes()?;
let mut pak_writer = pak_writer_mtx.lock(); // let mut pak_writer = pak_writer_mtx.lock();
let write_bytes = write_to_pak( // let write_bytes = write_to_pak(
&mut pak_writer, // &mut pak_writer,
entry, // entry,
entry.hash(), // entry.hash(),
&tex_bytes, // &tex_bytes,
use_feature_clone, // use_feature_clone,
)?; // )?;
bytes_written.fetch_add(write_bytes, Ordering::SeqCst); // bytes_written.fetch_add(write_bytes, Ordering::SeqCst);
} // }
bar.inc(1); // bar.inc(1);
if bar.position() % 100 == 0 { // if bar.position() % 100 == 0 {
bar.set_message( // bar.set_message(
HumanBytes(bytes_written.load(Ordering::SeqCst) as u64).to_string(), // HumanBytes(bytes_written.load(Ordering::SeqCst) as u64).to_string(),
); // );
} // }
Ok(()) // Ok(())
}); // });
if let Err(e) = err { // if let Err(e) = err {
eprintln!("Error occurred when processing tex: {e}"); // eprintln!("Error occurred when processing tex: {e}");
eprintln!( // eprintln!(
"The process terminated early, we'll save the current processed tex files to pak file." // "The process terminated early, we'll save the current processed tex files to pak file."
); // );
} // }
match Arc::try_unwrap(pak_writer_mtx) { // match Arc::try_unwrap(pak_writer_mtx) {
Ok(pak_writer) => pak_writer.into_inner().finish()?, // Ok(pak_writer) => pak_writer.into_inner().finish()?,
Err(_) => panic!("Arc::try_unwrap failed"), // Err(_) => panic!("Arc::try_unwrap failed"),
}; // };
bar.finish(); // bar.finish();
println!("{}", "Done!".cyan().bold()); // println!("{}", "Done!".cyan().bold());
if !use_full_package_mode { // if !use_full_package_mode {
println!( // println!(
"You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`." // "You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`."
); // );
} // }
Ok(()) // Ok(())
} // }
fn is_tex_file(hash: u64, file_name_table: &FileNameTable) -> bool { fn is_tex_file(hash: u64, file_name_table: &FileNameTable) -> bool {
let Some(file_name) = file_name_table.get_file_name(hash) else { let Some(file_name) = file_name_table.get_file_name(hash) else {