use anyhow::{Context, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use super::PakManifest;
#[derive(Debug, Serialize, Deserialize)]
pub struct ManifestIndex {
pub version: String,
pub source: String,
pub extract_path: String,
pub files: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemPool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub referenced_by: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub contains: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemStats {
pub name: String,
pub category: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rarity: Option<String>,
pub stats: HashMap<String, Vec<StatModifier>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub drop_pools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatModifier {
pub modifier_type: String,
pub index: u32,
pub guid: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ItemsDatabase {
pub version: String,
pub generated: String,
pub item_pools: HashMap<String, ItemPool>,
pub items: Vec<ItemStats>,
pub stats_summary: StatsSummary,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StatsSummary {
pub total_items: usize,
pub total_pools: usize,
pub stat_types: Vec<String>,
pub categories: Vec<String>,
pub manufacturers: Vec<String>,
}
pub fn extract_manifest(extract_dir: &Path, output_dir: &Path) -> Result<()> {
fs::create_dir_all(output_dir).context("Failed to create output directory")?;
println!("Extracting manifest from {:?}", extract_dir);
println!("Output directory: {:?}", output_dir);
print!("Extracting manufacturers...");
let manufacturers = super::extract_manufacturers(extract_dir);
let mfr_path = output_dir.join("manufacturers.json");
fs::write(&mfr_path, serde_json::to_string_pretty(&manufacturers)?)?;
println!(" {} entries", manufacturers.len());
print!("Extracting weapon types...");
let weapon_types = super::extract_weapon_types(extract_dir);
let wt_path = output_dir.join("weapon_types.json");
fs::write(&wt_path, serde_json::to_string_pretty(&weapon_types)?)?;
println!(" {} entries", weapon_types.len());
print!("Extracting balance data...");
let balance_data = super::extract_balance_data(extract_dir)?;
let bd_path = output_dir.join("balance_data.json");
fs::write(&bd_path, serde_json::to_string_pretty(&balance_data)?)?;
println!(" {} categories", balance_data.len());
print!("Extracting naming data...");
let naming_data = super::extract_naming_data(extract_dir)?;
let nd_path = output_dir.join("naming.json");
fs::write(&nd_path, serde_json::to_string_pretty(&naming_data)?)?;
println!(" {} entries", naming_data.len());
print!("Extracting gear types...");
let gear_types = super::extract_gear_types(extract_dir);
let gt_path = output_dir.join("gear_types.json");
fs::write(>_path, serde_json::to_string_pretty(&gear_types)?)?;
println!(" {} types", gear_types.len());
print!("Extracting rarity data...");
let rarity_data = super::extract_rarity_data(extract_dir);
let rd_path = output_dir.join("rarity.json");
fs::write(&rd_path, serde_json::to_string_pretty(&rarity_data)?)?;
println!(" {} entries", rarity_data.len());
print!("Extracting elemental data...");
let elemental_data = super::extract_elemental_data(extract_dir);
let ed_path = output_dir.join("elemental.json");
fs::write(&ed_path, serde_json::to_string_pretty(&elemental_data)?)?;
println!(" {} entries", elemental_data.len());
let mut files = HashMap::new();
files.insert(
"manufacturers".to_string(),
"manufacturers.json".to_string(),
);
files.insert("weapon_types".to_string(), "weapon_types.json".to_string());
files.insert("balance_data".to_string(), "balance_data.json".to_string());
files.insert("naming".to_string(), "naming.json".to_string());
files.insert("gear_types".to_string(), "gear_types.json".to_string());
files.insert("rarity".to_string(), "rarity.json".to_string());
files.insert("elemental".to_string(), "elemental.json".to_string());
let index = ManifestIndex {
version: env!("CARGO_PKG_VERSION").to_string(),
source: "BL4 Game Files".to_string(),
extract_path: extract_dir.to_string_lossy().to_string(),
files,
};
let index_path = output_dir.join("index.json");
fs::write(&index_path, serde_json::to_string_pretty(&index)?)?;
println!("\nManifest saved to {:?}", output_dir);
Ok(())
}
pub fn extract_item_pools(manifest_dir: &Path) -> Result<HashMap<String, ItemPool>> {
let pak_manifest_path = manifest_dir.join("pak_manifest.json");
if !pak_manifest_path.exists() {
anyhow::bail!("pak_manifest.json not found in {:?}", manifest_dir);
}
let content = fs::read_to_string(&pak_manifest_path)?;
let manifest: PakManifest = serde_json::from_str(&content)?;
let mut pools: HashMap<String, ItemPool> = HashMap::new();
let pool_pattern = Regex::new(r"(?:CItemPoolDef::)?[Ii]tem[Pp]ool[_A-Za-z0-9]*").unwrap();
for item in &manifest.items {
let asset_path = &item.path;
let asset_name = &item.asset_name;
for prop_str in &item.property_names {
for cap in pool_pattern.find_iter(prop_str) {
let pool_name = cap
.as_str()
.trim_start_matches("CItemPoolDef::")
.to_string();
let pool = pools.entry(pool_name.clone()).or_insert_with(|| ItemPool {
name: pool_name.clone(),
path: None,
referenced_by: Vec::new(),
contains: Vec::new(),
});
if !pool.referenced_by.contains(asset_name) {
pool.referenced_by.push(asset_name.clone());
}
if asset_name
.to_lowercase()
.contains(&pool_name.to_lowercase())
{
pool.path = Some(asset_path.clone());
}
}
}
}
Ok(pools)
}
pub fn extract_item_stats(manifest_dir: &Path) -> Result<Vec<ItemStats>> {
let pak_manifest_path = manifest_dir.join("pak_manifest.json");
if !pak_manifest_path.exists() {
anyhow::bail!("pak_manifest.json not found in {:?}", manifest_dir);
}
let content = fs::read_to_string(&pak_manifest_path)?;
let manifest: PakManifest = serde_json::from_str(&content)?;
let mut items: Vec<ItemStats> = Vec::new();
let stat_pattern =
Regex::new(r"^([A-Za-z]+[A-Za-z0-9]*)_(Scale|Add|Value|Percent)_(\d+)_([A-F0-9]{32})$")
.unwrap();
let rarity_pattern = Regex::new(r"comp_0([1-5])").unwrap();
let rarities = ["Common", "Uncommon", "Rare", "Epic", "Legendary"];
for item in &manifest.items {
if item.category == "unknown" && !item.path.to_lowercase().contains("gear") {
continue;
}
let mut stats: HashMap<String, Vec<StatModifier>> = HashMap::new();
let mut rarity: Option<String> = None;
for prop in &item.property_names {
if let Some(cap) = stat_pattern.captures(prop) {
let stat_name = cap[1].to_string();
let modifier_type = cap[2].to_string();
let index: u32 = cap[3].parse().unwrap_or(0);
let guid = cap[4].to_string();
let key = format!("{}_{}", stat_name, modifier_type);
stats.entry(key).or_default().push(StatModifier {
modifier_type,
index,
guid,
});
}
if rarity.is_none() {
if let Some(cap) = rarity_pattern.captures(prop) {
let tier: usize = cap[1].parse().unwrap_or(1);
if (1..=5).contains(&tier) {
rarity = Some(rarities[tier - 1].to_string());
}
}
}
}
let manufacturer = {
let path_lower = item.path.to_lowercase();
let codes = [
"BOR", "DAD", "DPL", "JAK", "MAL", "ORD", "RIP", "TED", "TOR", "VLA", "COV",
];
let mut found = None;
for code in codes {
let code_lower = code.to_lowercase();
if path_lower.contains(&format!("/{}/", code_lower))
|| path_lower.contains(&format!("/{}_", code_lower))
|| path_lower.contains(&format!("_{}_", code_lower))
{
found = Some(code.to_string());
break;
}
}
found
};
if !stats.is_empty() {
items.push(ItemStats {
name: item.asset_name.clone(),
category: item.category.clone(),
manufacturer,
rarity,
stats,
drop_pools: Vec::new(), });
}
}
Ok(items)
}
pub fn generate_items_database(manifest_dir: &Path) -> Result<ItemsDatabase> {
eprintln!("Extracting item pools...");
let item_pools = extract_item_pools(manifest_dir)?;
eprintln!(" Found {} unique pools", item_pools.len());
eprintln!("Extracting item stats...");
let items = extract_item_stats(manifest_dir)?;
eprintln!(" Found {} items with stats", items.len());
let mut stat_types: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut categories: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut manufacturers: std::collections::HashSet<String> = std::collections::HashSet::new();
for item in &items {
categories.insert(item.category.clone());
if let Some(ref mfr) = item.manufacturer {
manufacturers.insert(mfr.clone());
}
for key in item.stats.keys() {
if let Some(stat_name) = key.split('_').next() {
stat_types.insert(stat_name.to_string());
}
}
}
let stats_summary = StatsSummary {
total_items: items.len(),
total_pools: item_pools.len(),
stat_types: stat_types.into_iter().collect(),
categories: categories.into_iter().collect(),
manufacturers: manufacturers.into_iter().collect(),
};
Ok(ItemsDatabase {
version: env!("CARGO_PKG_VERSION").to_string(),
generated: chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string(),
item_pools,
items,
stats_summary,
})
}