8 Commits

Author SHA1 Message Date
ad98236e06 colorful and better interaction
Use colored texts.
Default enable feature clone.
Add panic hook to avoid program exit.
2025-03-30 21:20:05 +08:00
9d703833e4 Merge branch 'main' of github.com:eigeen/mhws-tex-decompressor 2025-03-30 20:56:03 +08:00
40f9ea4572 wait for exit when success 2025-03-30 20:55:32 +08:00
3009d73726 Merge pull request #2 from xuanplus/patch-1
Update main.rs to remove quotes
2025-03-22 11:15:39 +08:00
Haoxuan Di
108d8bce92 Update main.rs
remove quotes for dropping file to terminal
2025-03-22 11:11:43 +08:00
527354a3af full package and feature clone 2025-03-20 11:06:22 +08:00
f46a06948c pak entry flags clone 2025-03-19 21:30:28 +08:00
2b214cec01 fixed: a fixed input file path 2025-03-19 19:37:58 +08:00
4 changed files with 138 additions and 50 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target /target
*.exe
*.zip

16
Cargo.lock generated
View File

@@ -60,6 +60,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colored"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.11" version = "0.15.11"
@@ -295,8 +304,9 @@ checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]] [[package]]
name = "mhws-tex-decompressor" name = "mhws-tex-decompressor"
version = "0.1.0" version = "0.1.3"
dependencies = [ dependencies = [
"colored",
"dialoguer", "dialoguer",
"eyre", "eyre",
"indicatif", "indicatif",
@@ -494,7 +504,6 @@ dependencies = [
[[package]] [[package]]
name = "re-tex" name = "re-tex"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/eigeen/re-tex.git?branch=main#8d0278fa1064791e2e58049c55f9c9b784b53d62"
dependencies = [ dependencies = [
"better_default", "better_default",
"byteorder", "byteorder",
@@ -514,8 +523,7 @@ dependencies = [
[[package]] [[package]]
name = "ree-pak-core" name = "ree-pak-core"
version = "0.3.1" version = "0.4.0"
source = "git+https://github.com/eigeen/ree-pak-rs.git?branch=main#0f8e027f7dc60d3be950d403c8ae8c74c87b9798"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",

View File

@@ -1,18 +1,19 @@
[package] [package]
name = "mhws-tex-decompressor" name = "mhws-tex-decompressor"
version = "0.1.0" version = "0.1.3"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
# # local development # local development
# re-tex = { path = "../re-tex" } re-tex = { path = "../re-tex" }
# ree-pak-core = { path = "../../ree-pak-rs/ree-pak-core" } ree-pak-core = { path = "../../ree-pak-rs/ree-pak-core" }
re-tex = { git = "https://github.com/eigeen/re-tex.git", branch = "main" } # re-tex = { git = "https://github.com/eigeen/re-tex.git", branch = "main" }
ree-pak-core = { git = "https://github.com/eigeen/ree-pak-rs.git", branch = "main" } # ree-pak-core = { git = "https://github.com/eigeen/ree-pak-rs.git", branch = "main" }
dialoguer = "0.11.0" dialoguer = "0.11"
eyre = "0.6.12" eyre = "0.6"
indicatif = "0.17.11" indicatif = "0.17"
rayon = "1.10.0" rayon = "1.10"
parking_lot = "0.12.3" parking_lot = "0.12"
colored = "3.0"

View File

@@ -9,23 +9,38 @@ use std::{
time::Duration, time::Duration,
}; };
use dialoguer::{Input, theme::ColorfulTheme}; use colored::Colorize;
use dialoguer::{Input, Select, theme::ColorfulTheme};
use indicatif::{HumanBytes, ProgressBar, ProgressStyle}; use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
use parking_lot::Mutex; use parking_lot::Mutex;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use re_tex::tex::Tex; use re_tex::tex::Tex;
use ree_pak_core::{filename::FileNameTable, read::archive::PakArchiveReader, write::FileOptions}; 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"); const FILE_NAME_LIST: &[u8] = include_bytes!("../assets/MHWs_STM_Release.list.zst");
fn main() { fn main() {
println!("Version {}. Tool by @Eigeen", env!("CARGO_PKG_VERSION")); std::panic::set_hook(Box::new(panic_hook));
println!("Version v{} - Tool by @Eigeen", env!("CARGO_PKG_VERSION"));
if let Err(e) = main_entry() { if let Err(e) = main_entry() {
eprintln!("Error: {e}"); eprintln!("{}: {}", "Error".red().bold(), e);
wait_for_exit(); wait_for_exit();
std::process::exit(1); std::process::exit(1);
} }
wait_for_exit();
}
fn panic_hook(info: &std::panic::PanicHookInfo) {
eprintln!("{}: {}", "Panic".red().bold(), info);
wait_for_exit();
std::process::exit(1);
} }
fn main_entry() -> eyre::Result<()> { fn main_entry() -> eyre::Result<()> {
@@ -34,20 +49,37 @@ fn main_entry() -> eyre::Result<()> {
.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 == '\'')
.to_string();
println!("Input file: {}", input);
let input_path = Path::new(&input); let input_path = Path::new(&input);
if !input_path.is_file() { if !input_path.is_file() {
eyre::bail!("input file not exists."); eyre::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..."); 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_path = let file = fs::File::open(input_path)?;
"E:/SteamLibrary/steamapps/common/MonsterHunterWilds/re_chunk_000.pak.sub_000.pak";
let file = fs::File::open(file_path)?;
let mut reader = io::BufReader::new(file); let mut reader = io::BufReader::new(file);
println!("Reading pak archive..."); println!("Reading pak archive...");
@@ -56,17 +88,16 @@ fn main_entry() -> eyre::Result<()> {
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 {
pak_archive.entries().iter().collect::<Vec<_>>()
} else {
println!("Filtering entries..."); println!("Filtering entries...");
let entries = pak_archive pak_archive
.entries() .entries()
.iter() .iter()
.filter(|entry| { .filter(|entry| is_tex_file(entry.hash(), &filename_table))
let Some(file_name) = filename_table.get_file_name(entry.hash()) else { .collect::<Vec<_>>()
return false;
}; };
file_name.get_name().ends_with(".tex.241106027")
})
.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");
@@ -99,20 +130,37 @@ fn main_entry() -> eyre::Result<()> {
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) {
// 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)?; 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()?;
bytes_written.fetch_add(tex_bytes.len() as usize, Ordering::SeqCst);
// save file
let file_name = filename_table.get_file_name(entry.hash()).unwrap().clone();
{
let mut pak_writer = pak_writer_mtx.lock(); let mut pak_writer = pak_writer_mtx.lock();
pak_writer.start_file(file_name, FileOptions::default())?; let write_bytes = write_to_pak(
pak_writer.write_all(&tex_bytes)?; &mut pak_writer,
entry,
entry.hash(),
&tex_bytes,
use_feature_clone,
)?;
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(
@@ -135,16 +183,45 @@ fn main_entry() -> eyre::Result<()> {
}; };
bar.finish(); bar.finish();
println!("Done!"); println!("{}", "Done!".cyan().bold());
if !use_full_package_mode {
println!( println!(
"You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`, or manage it by your favorite mod manager." "You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`, or manage it by your favorite mod manager."
); );
}
Ok(()) 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,
) -> 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() { fn wait_for_exit() {
let _: String = Input::with_theme(&ColorfulTheme::default()) let _: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Press Enter to exit")
.allow_empty(true) .allow_empty(true)
.interact_text() .interact_text()
.unwrap(); .unwrap();