#![allow(clippy::doc_markdown)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub type MergedProgressCallback<'a> = &'a (dyn Fn(&MergedProgress) + Sync + Send);
#[derive(Debug, Clone)]
pub struct MergedProgress {
pub phase: MergedPhase,
pub current: usize,
pub total: usize,
pub current_file: Option<String>,
}
impl MergedProgress {
#[must_use]
pub fn new(phase: MergedPhase, current: usize, total: usize) -> Self {
Self {
phase,
current,
total,
current_file: None,
}
}
#[must_use]
pub fn with_file(
phase: MergedPhase,
current: usize,
total: usize,
file: impl Into<String>,
) -> Self {
Self {
phase,
current,
total,
current_file: Some(file.into()),
}
}
#[must_use]
pub fn percentage(&self) -> f32 {
if self.total == 0 {
1.0
} else {
self.current as f32 / self.total as f32
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergedPhase {
ScanningFiles,
ExtractingFiles,
ParsingLsf,
MergingData,
ResolvingReferences,
Complete,
}
impl MergedPhase {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::ScanningFiles => "Scanning files",
Self::ExtractingFiles => "Extracting files",
Self::ParsingLsf => "Parsing LSF files",
Self::MergingData => "Merging data",
Self::ResolvingReferences => "Resolving references",
Self::Complete => "Complete",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisualAsset {
pub id: String,
pub name: String,
pub gr2_path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_pak: String,
pub material_ids: Vec<String>,
pub textures: Vec<TextureRef>,
pub virtual_textures: Vec<VirtualTextureRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextureRef {
pub id: String,
pub name: String,
pub dds_path: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_pak: String,
pub width: u32,
pub height: u32,
pub parameter_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualTextureRef {
pub id: String,
pub name: String,
pub gtex_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct PakPaths {
pub models: String,
pub textures: String,
pub virtual_textures: String,
pub gtp_path_pattern: String,
}
impl PakPaths {
#[must_use]
pub fn bg3_default() -> Self {
Self {
models: "Models.pak".to_string(),
textures: "Textures.pak".to_string(),
virtual_textures: "VirtualTextures.pak".to_string(),
gtp_path_pattern:
"Generated/Public/VirtualTextures/Albedo_Normal_Physical_{first}_{hash}.gtp"
.to_string(),
}
}
#[must_use]
pub fn gtp_path_from_hash(&self, gtex_hash: &str) -> String {
if gtex_hash.is_empty() {
return String::new();
}
let first = >ex_hash[0..1];
self.gtp_path_pattern
.replace("{first}", first)
.replace("{hash}", gtex_hash)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MaterialDef {
pub id: String,
pub name: String,
pub source_file: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_pak: String,
pub texture_ids: Vec<TextureParam>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub virtual_texture_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextureParam {
pub name: String,
pub texture_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MergedDatabase {
pub source_path: String,
pub(crate) pak_paths: PakPaths,
pub visuals_by_id: HashMap<String, VisualAsset>,
pub visuals_by_name: HashMap<String, String>,
pub visuals_by_gr2: HashMap<String, Vec<String>>,
pub(crate) materials: HashMap<String, MaterialDef>,
pub textures: HashMap<String, TextureRef>,
pub virtual_textures: HashMap<String, VirtualTextureRef>,
}
impl MergedDatabase {
pub fn new(source_path: impl Into<String>) -> Self {
Self {
source_path: source_path.into(),
pak_paths: PakPaths::bg3_default(),
..Default::default()
}
}
#[must_use]
pub fn get_by_visual_name(&self, visual_name: &str) -> Option<&VisualAsset> {
let id = self.visuals_by_name.get(visual_name)?;
self.visuals_by_id.get(id)
}
#[must_use]
pub fn get_visuals_for_gr2(&self, gr2_name: &str) -> Vec<&VisualAsset> {
let filename = std::path::Path::new(gr2_name)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(gr2_name);
let ids = self.visuals_by_gr2.get(filename).or_else(|| {
let upper = filename.to_uppercase();
if upper != filename {
return self.visuals_by_gr2.get(&upper);
}
if let Some(stem) = filename.strip_suffix(".gr2") {
let with_upper_ext = format!("{stem}.GR2");
return self.visuals_by_gr2.get(&with_upper_ext);
}
None
});
ids.map(|ids| {
ids.iter()
.filter_map(|id| self.visuals_by_id.get(id))
.collect()
})
.unwrap_or_default()
}
pub fn visual_names(&self) -> impl Iterator<Item = &str> {
self.visuals_by_name.keys().map(std::string::String::as_str)
}
pub fn gr2_files(&self) -> impl Iterator<Item = &str> {
self.visuals_by_gr2.keys().map(std::string::String::as_str)
}
#[must_use]
pub fn stats(&self) -> DatabaseStats {
DatabaseStats {
visual_count: self.visuals_by_id.len(),
material_count: self.materials.len(),
texture_count: self.textures.len(),
virtual_texture_count: self.virtual_textures.len(),
}
}
pub fn import_materials_from(&mut self, other: &MergedDatabase) {
for (id, material) in &other.materials {
if !self.materials.contains_key(id) {
self.materials.insert(id.clone(), material.clone());
}
}
for (id, texture) in &other.textures {
if !self.textures.contains_key(id) {
self.textures.insert(id.clone(), texture.clone());
}
}
for (id, vt) in &other.virtual_textures {
if !self.virtual_textures.contains_key(id) {
self.virtual_textures.insert(id.clone(), vt.clone());
}
}
}
pub fn resolve_references(&mut self) {
let materials = self.materials.clone();
let textures = self.textures.clone();
let virtual_textures = self.virtual_textures.clone();
for visual in self.visuals_by_id.values_mut() {
let mut resolved_textures = Vec::new();
let mut resolved_vts = Vec::new();
for mat_id in &visual.material_ids {
if let Some(material) = materials.get(mat_id) {
for tex_param in &material.texture_ids {
if let Some(texture) = textures.get(&tex_param.texture_id) {
let mut tex_ref = texture.clone();
tex_ref.parameter_name = Some(tex_param.name.clone());
if !resolved_textures
.iter()
.any(|t: &TextureRef| t.id == tex_ref.id)
{
resolved_textures.push(tex_ref);
}
}
}
}
}
for mat_id in &visual.material_ids {
if let Some(material) = materials.get(mat_id) {
for vt_id in &material.virtual_texture_ids {
if let Some(vt) = virtual_textures.get(vt_id)
&& !resolved_vts
.iter()
.any(|v: &VirtualTextureRef| v.id == vt.id)
{
resolved_vts.push(vt.clone());
}
}
}
}
visual.textures = resolved_textures;
visual.virtual_textures = resolved_vts;
}
}
}
#[derive(Debug, Clone)]
pub struct DatabaseStats {
pub visual_count: usize,
pub material_count: usize,
pub texture_count: usize,
pub virtual_texture_count: usize,
}
#[derive(Debug, Clone)]
pub struct GtpMatch {
pub gtex_hash: String,
pub gtp_path: String,
pub pak_path: PathBuf,
}