use crate::error::{Error, Result};
use crate::merged::{MergedDatabase, VirtualTextureRef};
use crate::pak::PakOperations;
use crate::virtual_texture::VirtualTextureExtractor;
use std::path::{Path, PathBuf};
pub fn extract_virtual_textures(
virtual_textures: &[&VirtualTextureRef],
db: &MergedDatabase,
vt_source_path: Option<&Path>,
game_data: &Path,
output_dir: &Path,
) -> Result<Vec<PathBuf>> {
let mut extracted_paths = Vec::new();
if virtual_textures.is_empty() {
return Ok(extracted_paths);
}
let use_pak = vt_source_path.is_none() || !vt_source_path.unwrap().exists();
let vt_pak_path = game_data.join("VirtualTextures.pak");
if use_pak && !vt_pak_path.exists() {
tracing::warn!("VirtualTextures.pak not found: {}", vt_pak_path.display());
return Ok(extracted_paths);
}
let hashes: Vec<&str> = virtual_textures
.iter()
.filter(|vt| !vt.gtex_hash.is_empty())
.map(|vt| vt.gtex_hash.as_str())
.collect();
if use_pak {
let gtp_matches = find_gtp_files_in_pak(&vt_pak_path, &hashes)?;
tracing::info!(
"Found {} GTP matches in VirtualTextures.pak",
gtp_matches.len()
);
for vt in virtual_textures {
if vt.gtex_hash.is_empty() {
continue;
}
let gtp_match = gtp_matches.iter().find(|(hash, _)| *hash == vt.gtex_hash);
let Some((_, gtp_rel_path)) = gtp_match else {
tracing::warn!("GTP not found for hash {}", vt.gtex_hash);
continue;
};
let gts_rel_path = derive_gts_path(gtp_rel_path);
tracing::info!(
"Virtual texture {}: GTP={}, GTS={}",
vt.name,
gtp_rel_path,
gts_rel_path
);
match extract_virtual_texture_from_pak(
&vt_pak_path,
gtp_rel_path,
>s_rel_path,
&vt.name,
output_dir,
) {
Ok(paths) => extracted_paths.extend(paths),
Err(e) => {
tracing::warn!(
"Failed to extract virtual texture {} from PAK: {}",
vt.name,
e
);
}
}
}
} else {
let vt_source = vt_source_path.unwrap();
for vt in virtual_textures {
if vt.gtex_hash.is_empty() {
continue;
}
let gtp_rel_path = db.pak_paths.gtp_path_from_hash(&vt.gtex_hash);
if gtp_rel_path.is_empty() {
tracing::warn!("Could not derive GTP path for hash: {}", vt.gtex_hash);
continue;
}
let gts_rel_path = derive_gts_path(>p_rel_path);
let gtp_extracted_path = adjust_vt_path_for_extraction(>p_rel_path);
let gts_extracted_path = adjust_vt_path_for_extraction(>s_rel_path);
let gtp_path = vt_source.join(>p_extracted_path);
let gts_path = vt_source.join(>s_extracted_path);
if !gtp_path.exists() {
tracing::warn!("GTP file not found: {}", gtp_path.display());
continue;
}
if !gts_path.exists() {
tracing::warn!("GTS file not found: {}", gts_path.display());
continue;
}
match extract_and_rename_virtual_texture(>p_path, >s_path, &vt.name, output_dir) {
Ok(paths) => extracted_paths.extend(paths),
Err(e) => {
tracing::warn!("Failed to extract virtual texture {}: {}", vt.name, e);
}
}
}
}
Ok(extracted_paths)
}
pub fn find_gtp_files_in_pak(pak_path: &Path, hashes: &[&str]) -> Result<Vec<(String, String)>> {
let all_files = PakOperations::list(pak_path)?;
let mut matches = Vec::new();
for file_path in &all_files {
if !file_path.to_lowercase().ends_with(".gtp") {
continue;
}
let filename = Path::new(file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
let stem = filename.strip_suffix(".gtp").unwrap_or(filename);
for hash in hashes {
if stem.ends_with(hash) {
matches.push(((*hash).to_string(), file_path.clone()));
break;
}
}
}
Ok(matches)
}
pub fn extract_virtual_texture_from_pak(
vt_pak_path: &Path,
gtp_rel_path: &str,
gts_rel_path: &str,
vt_name: &str,
output_dir: &Path,
) -> Result<Vec<PathBuf>> {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let unique_id = COUNTER.fetch_add(1, Ordering::SeqCst);
let temp_dir =
std::env::temp_dir().join(format!("maclarian_vt_{}_{}", std::process::id(), unique_id));
std::fs::create_dir_all(&temp_dir)?;
tracing::info!(
"Reading virtual texture files from PAK: GTP={}, GTS={}",
gtp_rel_path,
gts_rel_path
);
let gtp_data = match PakOperations::read_file_bytes(vt_pak_path, gtp_rel_path) {
Ok(data) => data,
Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(Error::ConversionError(format!(
"Failed to read GTP from PAK: {}",
e
)));
}
};
let gts_data = match PakOperations::read_file_bytes(vt_pak_path, gts_rel_path) {
Ok(data) => data,
Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(Error::ConversionError(format!(
"Failed to read GTS from PAK: {}",
e
)));
}
};
let gtp_path = temp_dir.join(Path::new(gtp_rel_path).file_name().unwrap_or_default());
let gts_path = temp_dir.join(Path::new(gts_rel_path).file_name().unwrap_or_default());
std::fs::write(>p_path, >p_data)?;
std::fs::write(>s_path, >s_data)?;
tracing::info!(
"Wrote temp files: GTP={} ({} bytes), GTS={} ({} bytes)",
gtp_path.display(),
gtp_data.len(),
gts_path.display(),
gts_data.len()
);
let result = extract_and_rename_virtual_texture(>p_path, >s_path, vt_name, output_dir);
let _ = std::fs::remove_dir_all(&temp_dir);
result
}
pub fn extract_and_rename_virtual_texture(
gtp_path: &Path,
gts_path: &Path,
vt_name: &str,
output_dir: &Path,
) -> Result<Vec<PathBuf>> {
let mut extracted_paths = Vec::new();
VirtualTextureExtractor::extract_with_gts(gtp_path, gts_path, output_dir)?;
for layer in &["BaseMap", "NormalMap", "PhysicalMap"] {
let src = output_dir.join(format!("{layer}.dds"));
if src.exists() {
let dest = output_dir.join(format!("{}_{}.dds", vt_name, layer));
if std::fs::rename(&src, &dest).is_ok() {
extracted_paths.push(dest);
} else {
extracted_paths.push(src);
}
}
}
Ok(extracted_paths)
}
pub fn adjust_vt_path_for_extraction(path: &str) -> String {
let path_obj = std::path::Path::new(path);
let Some(filename) = path_obj.file_name().and_then(|f| f.to_str()) else {
return path.to_string();
};
let Some(parent) = path_obj.parent().and_then(|p| p.to_str()) else {
return path.to_string();
};
let stem = filename.trim_end_matches(".gtp").trim_end_matches(".gts");
let subfolder = if let Some(last_underscore) = stem.rfind('_') {
let suffix = &stem[last_underscore + 1..];
if suffix.len() == 32 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
&stem[..last_underscore]
} else {
stem
}
} else {
stem
};
format!("{parent}/{subfolder}/{filename}")
}
pub fn derive_gts_path(gtp_path: &str) -> String {
let without_ext = gtp_path.trim_end_matches(".gtp");
if let Some(last_underscore) = without_ext.rfind('_') {
let suffix = &without_ext[last_underscore + 1..];
if suffix.len() == 32 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
return format!("{}.gts", &without_ext[..last_underscore]);
}
}
format!("{without_ext}.gts")
}