auto mode support
This commit is contained in:
309
src/app.rs
309
src/app.rs
@@ -2,8 +2,8 @@ use std::{
|
|||||||
io::{self, Write},
|
io::{self, Write},
|
||||||
path::Path,
|
path::Path,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
},
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -11,8 +11,7 @@ use std::{
|
|||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
|
|
||||||
use color_eyre::eyre::bail;
|
use color_eyre::eyre::bail;
|
||||||
use colored::Colorize;
|
use dialoguer::{Input, MultiSelect, Select, theme::ColorfulTheme};
|
||||||
use dialoguer::{theme::ColorfulTheme, Input, Select};
|
|
||||||
use fs::OpenOptions;
|
use fs::OpenOptions;
|
||||||
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
|
use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
@@ -25,30 +24,27 @@ use ree_pak_core::{
|
|||||||
write::FileOptions,
|
write::FileOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILE_NAME_LIST: &[u8] = include_bytes!("../assets/MHWs_STM_Release.list.zst");
|
use crate::{chunk::ChunkName, util::human_bytes};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
const FILE_NAME_LIST: &[u8] = include_bytes!("../assets/MHWs_STM_Release.list.zst");
|
||||||
pub struct Config {
|
const AUTO_CHUNK_SELECTION_SIZE_THRESHOLD: usize = 50 * 1024 * 1024; // 50MB
|
||||||
pub input_path: Option<String>,
|
const FALSE_TRUE_SELECTION: [&str; 2] = ["False", "True"];
|
||||||
pub output_path: Option<String>,
|
|
||||||
pub use_full_package_mode: bool,
|
struct ChunkSelection {
|
||||||
pub use_feature_clone: bool,
|
chunk_name: ChunkName,
|
||||||
|
file_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl std::fmt::Display for ChunkSelection {
|
||||||
fn default() -> Self {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Self {
|
write!(f, "{} ({})", self.chunk_name, human_bytes(self.file_size))?;
|
||||||
input_path: Default::default(),
|
Ok(())
|
||||||
output_path: Default::default(),
|
|
||||||
use_full_package_mode: false,
|
|
||||||
use_feature_clone: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
config: Config,
|
filename_table: Option<FileNameTable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -57,6 +53,10 @@ impl App {
|
|||||||
println!("Get updates at https://github.com/eigeen/mhws-tex-decompressor");
|
println!("Get updates at https://github.com/eigeen/mhws-tex-decompressor");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
println!("Loading embedded file name table...");
|
||||||
|
let filename_table = FileNameTable::from_bytes(FILE_NAME_LIST)?;
|
||||||
|
self.filename_table = Some(filename_table);
|
||||||
|
|
||||||
// Mode selection
|
// Mode selection
|
||||||
let mode = Select::with_theme(&ColorfulTheme::default())
|
let mode = Select::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("Select mode")
|
.with_prompt("Select mode")
|
||||||
@@ -72,47 +72,19 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_mode(&mut self) -> color_eyre::Result<()> {
|
fn filename_table(&self) -> &FileNameTable {
|
||||||
todo!()
|
self.filename_table.as_ref().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn manual_mode(&mut self) -> color_eyre::Result<()> {
|
fn process_chunk(
|
||||||
let input: String = Input::with_theme(&ColorfulTheme::default())
|
&self,
|
||||||
.show_default(true)
|
filename_table: &FileNameTable,
|
||||||
.default("re_chunk_000.pak.sub_000.pak".to_string())
|
input_path: &Path,
|
||||||
.with_prompt("Input .pak file path")
|
output_path: &Path,
|
||||||
.interact_text()
|
use_full_package_mode: bool,
|
||||||
.unwrap()
|
use_feature_clone: bool,
|
||||||
.trim_matches(|c| c == '\"' || c == '\'')
|
) -> color_eyre::Result<()> {
|
||||||
.to_string();
|
println!("Processing chunk: {}", input_path.display());
|
||||||
|
|
||||||
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 file = fs::File::open(input_path)?;
|
||||||
let mut reader = io::BufReader::new(file);
|
let mut reader = io::BufReader::new(file);
|
||||||
@@ -135,8 +107,6 @@ impl App {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// new pak archive
|
// 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()
|
let out_file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
@@ -218,13 +188,218 @@ impl App {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bar.finish();
|
bar.finish();
|
||||||
println!("{}", "Done!".cyan().bold());
|
|
||||||
if !use_full_package_mode {
|
Ok(())
|
||||||
println!(
|
}
|
||||||
"You should rename the output file like `re_chunk_000.pak.sub_000.pak.patch_xxx.pak`."
|
|
||||||
);
|
fn auto_mode(&mut self) -> color_eyre::Result<()> {
|
||||||
|
let current_dir = std::env::current_dir()?;
|
||||||
|
|
||||||
|
wait_for_enter(
|
||||||
|
r#"Check list:
|
||||||
|
|
||||||
|
1. Your game is already updated to the latest version.
|
||||||
|
2. Uninstalled all the mods, or the generated files will break mods.
|
||||||
|
|
||||||
|
I'm sure I've checked the list, press Enter to continue"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let game_dir: String = Input::<String>::with_theme(&ColorfulTheme::default())
|
||||||
|
.show_default(true)
|
||||||
|
.default(current_dir.to_string_lossy().to_string())
|
||||||
|
.with_prompt("Input MonsterHunterWilds directory path")
|
||||||
|
.interact_text()
|
||||||
|
.unwrap()
|
||||||
|
.trim_matches(|c| c == '\"' || c == '\'')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let game_dir = Path::new(&game_dir);
|
||||||
|
if !game_dir.is_dir() {
|
||||||
|
bail!("game directory not exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scan for pak files
|
||||||
|
let dir = fs::read_dir(game_dir)?;
|
||||||
|
let mut all_chunks: Vec<ChunkName> = vec![];
|
||||||
|
for entry in dir {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type()?.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if !file_name.ends_with(".pak") || !file_name.starts_with("re_chunk_") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_name = match ChunkName::try_from_str(&file_name) {
|
||||||
|
Ok(chunk_name) => chunk_name,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Invalid chunk name, skipped: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
all_chunks.push(chunk_name);
|
||||||
|
}
|
||||||
|
all_chunks.sort();
|
||||||
|
|
||||||
|
// show chunks for selection
|
||||||
|
// only show sub chunks
|
||||||
|
let chunk_selections = all_chunks
|
||||||
|
.iter()
|
||||||
|
.filter_map(|chunk| {
|
||||||
|
if chunk.sub_id.is_some() {
|
||||||
|
Some(chunk.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|file_name| {
|
||||||
|
let file_path = game_dir.join(&file_name);
|
||||||
|
let file_size = fs::metadata(file_path)?.len();
|
||||||
|
Ok(ChunkSelection {
|
||||||
|
chunk_name: ChunkName::try_from_str(&file_name)?,
|
||||||
|
file_size,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<color_eyre::Result<Vec<_>>>()?;
|
||||||
|
if chunk_selections.is_empty() {
|
||||||
|
bail!("No available pak files found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected_chunks: Vec<bool> = chunk_selections
|
||||||
|
.iter()
|
||||||
|
.map(|chunk_selection| {
|
||||||
|
Ok(chunk_selection.file_size >= AUTO_CHUNK_SELECTION_SIZE_THRESHOLD as u64)
|
||||||
|
})
|
||||||
|
.collect::<color_eyre::Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let selected_chunks: Option<Vec<usize>> =
|
||||||
|
MultiSelect::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt("Select chunks to process (Space to select, Enter to confirm)")
|
||||||
|
.items(&chunk_selections)
|
||||||
|
.defaults(&selected_chunks)
|
||||||
|
.interact_opt()?;
|
||||||
|
let Some(selected_chunks) = selected_chunks else {
|
||||||
|
bail!("No chunks selected.");
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_chunks = selected_chunks
|
||||||
|
.iter()
|
||||||
|
.map(|i| chunk_selections[*i].chunk_name.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// replace mode: replace original files with uncompressed files
|
||||||
|
// patch mode: generate patch files after original patch files
|
||||||
|
let use_replace_mode = Select::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt(
|
||||||
|
"Replace original files with uncompressed files? (Will automatically backup original files)",
|
||||||
|
)
|
||||||
|
.default(0)
|
||||||
|
.items(&FALSE_TRUE_SELECTION)
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
let use_replace_mode = use_replace_mode == 1;
|
||||||
|
|
||||||
|
// start processing
|
||||||
|
for chunk_name in selected_chunks {
|
||||||
|
let chunk_path = game_dir.join(chunk_name.to_string());
|
||||||
|
let output_path = if use_replace_mode {
|
||||||
|
// 如果是替换模式,首先生成一个临时的解压后文件
|
||||||
|
let temp_path = chunk_path.with_extension("pak.temp");
|
||||||
|
// 备份原始文件
|
||||||
|
let backup_path = chunk_path.with_extension("pak.backup");
|
||||||
|
if backup_path.exists() {
|
||||||
|
fs::remove_file(&backup_path)?;
|
||||||
|
}
|
||||||
|
fs::rename(&chunk_path, &backup_path)?;
|
||||||
|
temp_path
|
||||||
|
} else {
|
||||||
|
// 如果是补丁模式
|
||||||
|
// 查找当前chunk系列的最大patch id
|
||||||
|
let max_patch_id = all_chunks
|
||||||
|
.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
c.major_id == chunk_name.major_id
|
||||||
|
&& c.patch_id == chunk_name.patch_id
|
||||||
|
&& c.sub_id == chunk_name.sub_id
|
||||||
|
})
|
||||||
|
.filter_map(|c| c.sub_patch_id)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let new_patch_id = max_patch_id + 1;
|
||||||
|
|
||||||
|
// 创建新的chunk name
|
||||||
|
let mut output_chunk_name = chunk_name.clone();
|
||||||
|
output_chunk_name.sub_patch_id = Some(new_patch_id);
|
||||||
|
|
||||||
|
// 在chunk列表中添加新的补丁,以便后续处理可以找到它
|
||||||
|
all_chunks.push(output_chunk_name.clone());
|
||||||
|
|
||||||
|
game_dir.join(output_chunk_name.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Output patch file: {}", output_path.display());
|
||||||
|
self.process_chunk(
|
||||||
|
self.filename_table(),
|
||||||
|
&chunk_path,
|
||||||
|
&output_path,
|
||||||
|
use_replace_mode,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 如果是替换模式,将临时文件重命名为原始文件名
|
||||||
|
if use_replace_mode {
|
||||||
|
fs::rename(&output_path, &chunk_path)?;
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
self.process_chunk(
|
||||||
|
self.filename_table(),
|
||||||
|
input_path,
|
||||||
|
&input_path.with_extension("uncompressed.pak"),
|
||||||
|
use_full_package_mode,
|
||||||
|
use_feature_clone,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,9 +430,9 @@ where
|
|||||||
Ok(data.len())
|
Ok(data.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_exit() {
|
fn wait_for_enter(msg: &str) {
|
||||||
let _: String = Input::with_theme(&ColorfulTheme::default())
|
let _: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("Press Enter to exit")
|
.with_prompt(msg)
|
||||||
.allow_empty(true)
|
.allow_empty(true)
|
||||||
.interact_text()
|
.interact_text()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
171
src/chunk.rs
171
src/chunk.rs
@@ -1,5 +1,174 @@
|
|||||||
//! Chunk file name format
|
//! Chunk file name format
|
||||||
|
//!
|
||||||
|
//! File name structure:
|
||||||
|
//! - Base: re_chunk_XXX.pak
|
||||||
|
//! - Patch: re_chunk_XXX.pak.patch_XXX.pak
|
||||||
|
//! - Sub: re_chunk_XXX.pak.sub_XXX.pak
|
||||||
|
//! - Sub Patch: re_chunk_XXX.pak.sub_XXX.pak.patch_XXX.pak
|
||||||
|
|
||||||
|
use color_eyre::eyre;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ChunkName {
|
pub struct ChunkName {
|
||||||
major_id: u32,
|
/// Major chunk ID (XXX in re_chunk_XXX.pak)
|
||||||
|
pub major_id: u32,
|
||||||
|
/// Patch number (XXX in .patch_XXX.pak)
|
||||||
|
pub patch_id: Option<u32>,
|
||||||
|
/// Sub chunk ID (XXX in .sub_XXX.pak)
|
||||||
|
pub sub_id: Option<u32>,
|
||||||
|
/// Patch number for sub chunk (YYY in .sub_XXX.pak.patch_YYY.pak)
|
||||||
|
pub sub_patch_id: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkName {
|
||||||
|
/// Create a new base chunk name (re_chunk_XXX.pak)
|
||||||
|
pub fn new(major_id: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
major_id,
|
||||||
|
patch_id: None,
|
||||||
|
sub_id: None,
|
||||||
|
sub_patch_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a chunk name from a string
|
||||||
|
pub fn try_from_str(name: &str) -> color_eyre::Result<Self> {
|
||||||
|
let dot_parts = name.split('.').collect::<Vec<&str>>();
|
||||||
|
if dot_parts.len() < 2 || dot_parts.len() % 2 != 0 {
|
||||||
|
return Err(eyre::eyre!(
|
||||||
|
"Invalid chunk name with odd number of parts: {}",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// every 2 parts is a component
|
||||||
|
let components = dot_parts
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.collect::<Vec<(&str, &str)>>();
|
||||||
|
// check if all parts have the correct extension
|
||||||
|
if !components.iter().all(|(_, ext)| *ext == "pak") {
|
||||||
|
return Err(eyre::eyre!(
|
||||||
|
"Invalid chunk name with invalid extension: {}",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut this = Self::new(0);
|
||||||
|
|
||||||
|
for (name, _) in components.iter() {
|
||||||
|
let component = Self::parse_component(name)?;
|
||||||
|
match component {
|
||||||
|
Component::Major(id) => this.major_id = id,
|
||||||
|
Component::Sub(id) => this.sub_id = Some(id),
|
||||||
|
Component::Patch(id) => {
|
||||||
|
if this.sub_id.is_some() {
|
||||||
|
this.sub_patch_id = Some(id);
|
||||||
|
} else {
|
||||||
|
this.patch_id = Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_component(name: &str) -> color_eyre::Result<Component> {
|
||||||
|
if name.starts_with("re_chunk_") {
|
||||||
|
let major_id = name
|
||||||
|
.strip_prefix("re_chunk_")
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|e| eyre::eyre!("Chunk name with invalid major ID: {}", e))?;
|
||||||
|
Ok(Component::Major(major_id))
|
||||||
|
} else if name.starts_with("patch_") {
|
||||||
|
let patch_id = name
|
||||||
|
.strip_prefix("patch_")
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|e| eyre::eyre!("Chunk name with invalid patch ID: {}", e))?;
|
||||||
|
Ok(Component::Patch(patch_id))
|
||||||
|
} else if name.starts_with("sub_") {
|
||||||
|
let sub_id = name
|
||||||
|
.strip_prefix("sub_")
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|e| eyre::eyre!("Chunk name with invalid sub ID: {}", e))?;
|
||||||
|
Ok(Component::Sub(sub_id))
|
||||||
|
} else {
|
||||||
|
Err(eyre::eyre!(
|
||||||
|
"Invalid chunk name with invalid component: {}",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ChunkName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "re_chunk_{:03}.pak", self.major_id)?;
|
||||||
|
if let Some(patch_id) = self.patch_id {
|
||||||
|
write!(f, ".patch_{:03}.pak", patch_id)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(sub_id) = self.sub_id {
|
||||||
|
write!(f, ".sub_{:03}.pak", sub_id)?;
|
||||||
|
}
|
||||||
|
if let Some(sub_patch_id) = self.sub_patch_id {
|
||||||
|
write!(f, ".patch_{:03}.pak", sub_patch_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for ChunkName {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for ChunkName {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.major_id
|
||||||
|
.cmp(&other.major_id)
|
||||||
|
.then(self.sub_id.cmp(&other.sub_id))
|
||||||
|
.then(self.patch_id.cmp(&other.patch_id))
|
||||||
|
.then(self.sub_patch_id.cmp(&other.sub_patch_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Component {
|
||||||
|
Major(u32),
|
||||||
|
Patch(u32),
|
||||||
|
Sub(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chunk_name_formats() {
|
||||||
|
// Test base chunk
|
||||||
|
let base = ChunkName::new(0);
|
||||||
|
assert_eq!(base.to_string(), "re_chunk_000.pak");
|
||||||
|
|
||||||
|
// Test patch chunk
|
||||||
|
let patch = ChunkName::try_from_str("re_chunk_000.pak.patch_001.pak").unwrap();
|
||||||
|
assert_eq!(patch.to_string(), "re_chunk_000.pak.patch_001.pak");
|
||||||
|
|
||||||
|
// Test sub chunk
|
||||||
|
let sub = ChunkName::try_from_str("re_chunk_000.pak.sub_000.pak").unwrap();
|
||||||
|
assert_eq!(sub.to_string(), "re_chunk_000.pak.sub_000.pak");
|
||||||
|
|
||||||
|
// Test sub patch chunk
|
||||||
|
let sub_patch =
|
||||||
|
ChunkName::try_from_str("re_chunk_000.pak.sub_000.pak.patch_001.pak").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sub_patch.to_string(),
|
||||||
|
"re_chunk_000.pak.sub_000.pak.patch_001.pak"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
213
src/main.rs
213
src/main.rs
@@ -1,32 +1,9 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod chunk;
|
mod chunk;
|
||||||
|
mod util;
|
||||||
|
|
||||||
use std::{
|
|
||||||
io::{self, Write},
|
|
||||||
path::Path,
|
|
||||||
sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicUsize, Ordering},
|
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use fs_err as fs;
|
|
||||||
|
|
||||||
use color_eyre::eyre::bail;
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Input, Select, theme::ColorfulTheme};
|
use dialoguer::{Input, theme::ColorfulTheme};
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
std::panic::set_hook(Box::new(panic_hook));
|
std::panic::set_hook(Box::new(panic_hook));
|
||||||
@@ -38,17 +15,6 @@ fn main() {
|
|||||||
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) {
|
||||||
@@ -57,181 +23,6 @@ fn panic_hook(info: &std::panic::PanicHookInfo) {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn main_entry() -> 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() {
|
fn wait_for_exit() {
|
||||||
let _: String = Input::with_theme(&ColorfulTheme::default())
|
let _: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("Press Enter to exit")
|
.with_prompt("Press Enter to exit")
|
||||||
|
5
src/util.rs
Normal file
5
src/util.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use indicatif::HumanBytes;
|
||||||
|
|
||||||
|
pub fn human_bytes(bytes: u64) -> String {
|
||||||
|
HumanBytes(bytes).to_string()
|
||||||
|
}
|
Reference in New Issue
Block a user