use crate::common::{read_current_installed_mods, settings_file_path, write_file};
use anyhow::Result;
use larian_formats::lspk::{self, DecompressedLspk};
use quick_xml::se::Serializer;
use serde::Serialize;
use std::{
fs::File,
path::{Path, PathBuf},
};
pub fn install_all(
verbose: bool,
dry_run: bool,
refresh: bool,
mod_file_paths: Vec<String>,
app_data_path: &str,
) -> Result<()> {
mod_file_paths.into_iter().try_for_each(|mod_file_path| {
println!("Installing {mod_file_path}");
install(verbose, dry_run, refresh, mod_file_path, app_data_path)
})
}
fn install(
verbose: bool,
dry_run: bool,
refresh: bool,
mut mod_file_path: String,
app_data_path: &str,
) -> Result<()> {
if mod_file_path.starts_with('~') {
let home_dir = std::env::var("HOME")?;
mod_file_path = mod_file_path.replacen('~', &home_dir, 1);
}
let mod_file = File::open(&mod_file_path)?;
let (name, lspk_files) = read_mod(&mod_file_path, mod_file)?;
if let Some(name) = name {
mod_file_path = name;
}
let lspk_meta_lsx = lspk_files.extract_meta_lsx()?;
let parsed_mod_metadata = lspk_meta_lsx.extract_meta_lsx_data()?;
let mod_info = parsed_mod_metadata.find_node_by_id("ModuleInfo")?;
let mod_name = mod_info.find_attribute_value_where_id(|id| id == "Name")?;
let mod_uuid = mod_info.find_attribute_value_where_id(|id| id == "UUID")?;
let mod_folder = mod_info.find_attribute_value_where_id(|id| id == "Folder")?;
let lspk_mod_version: u64 = mod_info
.find_attribute_value_where_id(|id| id == "Version" || id == "Version64")?
.parse()?;
let mut parsed_settings = read_current_installed_mods(app_data_path)?;
let mods = parsed_settings.find_node_mut_by_id("Mods")?;
let base_id = mods
.get_uuid_where_name("GustavDev")
.map(ToString::to_string);
let existing_version_node =
mods.get_or_insert_child_mut_where_id(mod_uuid, mod_name, mod_folder);
match existing_version_node.get_version() {
Some(existing) if existing > lspk_mod_version => {
println!(" {mod_name} version {existing} is already installed");
println!(
" The mod file you're trying to install is version {lspk_mod_version}, which \
is older."
);
println!(" \nNo installation needed!\n");
return Ok(());
}
Some(existing) if existing == lspk_mod_version && !refresh => {
println!(" {mod_name} version {existing} is already installed");
println!(" The mod file you're trying to install is the same version");
println!("\n No installation needed!\n");
return Ok(());
}
Some(existing) => {
if verbose {
println!(" updating {mod_name} from {existing} to {lspk_mod_version}");
}
}
None => {
if verbose {
println!(" installing {mod_name} version {lspk_mod_version}");
}
}
}
let lspk_installed_file_name = mod_file_path;
let lspk_installation_path: PathBuf = [
&app_data_path,
"Local",
"Larian Studios",
"Baldur's Gate 3",
"Mods",
&lspk_installed_file_name,
]
.into_iter()
.collect();
if !dry_run {
write_file(&lspk_installation_path, lspk_files.original_bytes, false)?;
}
existing_version_node.set_attribute_where_id(lspk_mod_version.to_string(), "int64", |id| {
id == "Version" || id == "Version64"
});
let mod_order = parsed_settings.find_node_mut_by_id("ModOrder")?;
if let Some(base_id) = base_id {
if dry_run && verbose {
println!(
" non-dry run would ensure that GustavDev ({base_id} is first is mod order"
);
} else {
if verbose {
println!(" ensuring that GustavDev ({base_id} is first is mod order");
}
mod_order.prepend_base(base_id);
}
}
mod_order.ensure_node_exists(mod_uuid);
let mut xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
let mut ser = Serializer::new(&mut xml);
ser.indent(' ', 4);
parsed_settings.serialize(ser)?;
xml.push('\n');
if dry_run {
print!(" non-dry run would have ");
} else {
write_file(settings_file_path(app_data_path), xml.replace("/>\n", " />\n"), true)?;
}
println!(" installed {}\n", lspk_installation_path.display());
Ok(())
}
fn read_mod(mod_file_path: &str, mod_file: File) -> Result<(Option<String>, DecompressedLspk)> {
let is_zipfile = Path::new(mod_file_path)
.extension()
.map_or(false, |e| e.eq_ignore_ascii_case("zip"));
if is_zipfile {
let (name, file) = lspk::entry_from_zipfile(&mod_file)?;
Ok((Some(name), file))
} else {
let file = lspk::Reader::new(mod_file)?.read()?;
Ok((None, file))
}
}