#![allow(
clippy::struct_excessive_bools,
clippy::collapsible_if,
clippy::doc_markdown,
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::option_if_let_else,
clippy::redundant_closure_for_method_calls,
clippy::uninlined_format_args,
clippy::return_self_not_must_use,
clippy::map_unwrap_or
)]
mod dds;
mod types;
mod virtual_textures;
pub(crate) use dds::extract_dds_textures;
pub use types::{
Gr2ExtractionOptions, Gr2ExtractionPhase, Gr2ExtractionProgress, Gr2ExtractionProgressCallback,
Gr2ExtractionResult,
};
pub(crate) use virtual_textures::extract_virtual_textures;
use crate::converter::{convert_dds_to_png, convert_gr2_to_glb};
use crate::error::{Error, Result};
use crate::merged::{
GameDataResolver, MergedDatabase, TextureRef, VirtualTextureRef, bg3_data_path,
};
use crate::pak::PakOperations;
use std::collections::HashSet;
use std::path::Path;
pub fn process_extracted_gr2(
gr2_path: &Path,
options: &Gr2ExtractionOptions,
) -> Result<Gr2ExtractionResult> {
let mut result = Gr2ExtractionResult {
gr2_path: gr2_path.to_path_buf(),
glb_path: None,
texture_paths: Vec::new(),
warnings: Vec::new(),
};
let output_dir = gr2_path
.parent()
.ok_or_else(|| Error::InvalidPath("GR2 path has no parent directory".to_string()))?;
if options.convert_to_glb {
let glb_path = gr2_path.with_extension("glb");
match convert_gr2_to_glb(gr2_path, &glb_path) {
Ok(()) => {
result.glb_path = Some(glb_path);
}
Err(e) => {
result
.warnings
.push(format!("Failed to convert to GLB: {e}"));
}
}
}
if options.extract_textures {
let resolver = if let Some(ref game_data) = options.bg3_path {
GameDataResolver::new(game_data).ok()
} else {
GameDataResolver::auto_detect().ok()
};
if let Some(resolver) = resolver {
let textures =
extract_textures_for_gr2(gr2_path, resolver.database(), output_dir, options)?;
if options.convert_to_png {
result.texture_paths =
convert_textures_to_png(&textures, options, &mut result.warnings);
} else {
result.texture_paths = textures;
}
} else {
result.warnings.push(
"Could not find BG3 install path for texture lookup. Use --bg3-path to specify the path.".to_string()
);
}
}
Ok(result)
}
pub fn process_extracted_gr2_to_dir(
gr2_path: &Path,
output_dir: &Path,
options: &Gr2ExtractionOptions,
) -> Result<Gr2ExtractionResult> {
let mut result = Gr2ExtractionResult {
gr2_path: gr2_path.to_path_buf(),
glb_path: None,
texture_paths: Vec::new(),
warnings: Vec::new(),
};
std::fs::create_dir_all(output_dir)
.map_err(|e| Error::ConversionError(format!("Failed to create output directory: {e}")))?;
if options.convert_to_glb {
let glb_name = gr2_path.file_stem().unwrap_or_default();
let glb_path = output_dir.join(format!("{}.glb", glb_name.to_string_lossy()));
match convert_gr2_to_glb(gr2_path, &glb_path) {
Ok(()) => {
result.glb_path = Some(glb_path);
}
Err(e) => {
result
.warnings
.push(format!("Failed to convert to GLB: {e}"));
}
}
}
if options.extract_textures {
let resolver = if let Some(ref game_data) = options.bg3_path {
GameDataResolver::new(game_data).ok()
} else {
GameDataResolver::auto_detect().ok()
};
if let Some(resolver) = resolver {
let textures =
extract_textures_for_gr2(gr2_path, resolver.database(), output_dir, options)?;
if options.convert_to_png {
result.texture_paths =
convert_textures_to_png(&textures, options, &mut result.warnings);
} else {
result.texture_paths = textures;
}
} else {
result.warnings.push(
"Could not find BG3 install path for texture lookup. Use --bg3-path to specify the path.".to_string()
);
}
}
Ok(result)
}
pub fn extract_gr2_with_textures(
source_pak: &Path,
gr2_path_in_pak: &str,
output_dir: &Path,
options: &Gr2ExtractionOptions,
) -> Result<Gr2ExtractionResult> {
PakOperations::extract_files(source_pak, output_dir, &[gr2_path_in_pak])?;
let extracted_gr2 = output_dir.join(gr2_path_in_pak);
if !extracted_gr2.exists() {
return Err(Error::ConversionError(format!(
"GR2 file not found after extraction: {}",
extracted_gr2.display()
)));
}
process_extracted_gr2(&extracted_gr2, options)
}
fn extract_textures_for_gr2(
gr2_path: &Path,
db: &MergedDatabase,
output_dir: &Path,
options: &Gr2ExtractionOptions,
) -> Result<Vec<std::path::PathBuf>> {
let mut extracted_paths = Vec::new();
let gr2_filename = gr2_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| Error::ConversionError("Invalid GR2 filename".to_string()))?;
let visuals = db.get_visuals_for_gr2(gr2_filename);
tracing::info!(
"Found {} visuals for GR2 '{}' in database",
visuals.len(),
gr2_filename
);
if visuals.is_empty() {
return Ok(extracted_paths);
}
let game_data = options
.bg3_path
.clone()
.or_else(bg3_data_path)
.ok_or_else(|| {
Error::ConversionError("Could not determine BG3 install path".to_string())
})?;
let mut seen_textures: HashSet<String> = HashSet::new();
let mut textures_to_extract: Vec<&TextureRef> = Vec::new();
let mut seen_virtual_textures: HashSet<String> = HashSet::new();
let mut virtual_textures_to_extract: Vec<&VirtualTextureRef> = Vec::new();
for visual in &visuals {
for texture in &visual.textures {
if seen_textures.insert(texture.id.clone()) {
textures_to_extract.push(texture);
}
}
for vt in &visual.virtual_textures {
if seen_virtual_textures.insert(vt.id.clone()) {
virtual_textures_to_extract.push(vt);
}
}
}
tracing::info!(
"Textures to extract: {} regular, {} virtual",
textures_to_extract.len(),
virtual_textures_to_extract.len()
);
extracted_paths.extend(extract_dds_textures(
&textures_to_extract,
&game_data,
output_dir,
)?);
if !virtual_textures_to_extract.is_empty() {
extracted_paths.extend(extract_virtual_textures(
&virtual_textures_to_extract,
db,
options.virtual_textures_path.as_deref(),
&game_data,
output_dir,
)?);
}
Ok(extracted_paths)
}
fn convert_textures_to_png(
textures: &[std::path::PathBuf],
options: &Gr2ExtractionOptions,
warnings: &mut Vec<String>,
) -> Vec<std::path::PathBuf> {
let mut png_paths = Vec::new();
for dds_path in textures {
let is_dds = dds_path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("dds"))
.unwrap_or(false);
if is_dds {
let png_path = dds_path.with_extension("png");
match convert_dds_to_png(dds_path, &png_path) {
Ok(()) => {
tracing::info!("Converted {} to PNG", dds_path.display());
if !options.keep_original_dds {
let _ = std::fs::remove_file(dds_path);
}
png_paths.push(png_path);
}
Err(e) => {
warnings.push(format!(
"Failed to convert {} to PNG: {}",
dds_path.display(),
e
));
png_paths.push(dds_path.clone());
}
}
} else {
png_paths.push(dds_path.clone());
}
}
png_paths
}