oliver 0.1.9

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
Documentation
use crate::Settings;
use anyhow::Result;
use larian_formats::lspk::{self, DecompressedLspk};
use std::{
    fs::{File, OpenOptions},
    io::Write,
    path::Path,
};

impl Settings {
    pub(crate) fn install_all_inner(
        &self,
        verbose: bool,
        dry_run: bool,
        refresh: bool,
        mod_file_paths: Vec<impl AsRef<Path>>,
    ) -> Result<()> {
        mod_file_paths.into_iter().try_for_each(|mod_file_path| {
            let mod_file_path = mod_file_path.as_ref().display().to_string();

            println!("Installing {mod_file_path}");

            self.install(verbose, dry_run, refresh, mod_file_path)
        })
    }

    fn install(
        &self,
        verbose: bool,
        dry_run: bool,
        refresh: bool,
        mut mod_file_path: String,
    ) -> 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 decompressed_lspk = read_mod(&mod_file_path, mod_file)?;
        let module_info = decompressed_lspk
            .clone()
            .extract_meta_lsx()?
            .deserialize_as_mod_pak()?
            .module_info;

        let mut parsed_settings = self.read_current_installed_mods()?;

        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(
            &module_info.uuid,
            &module_info.name,
            &module_info.folder,
        );

        match existing_version_node.get_version() {
            Some(existing) if existing > module_info.version => {
                println!("    {} version {existing} is already installed", module_info.name);
                println!(
                    "    The mod file you're trying to install is version {}, which is older.",
                    module_info.version
                );
                println!("    \nNo installation needed!\n");

                return Ok(());
            }
            Some(existing) if existing == module_info.version && !refresh => {
                println!("    {} version {existing} is already installed", module_info.name);
                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 {} from {existing} to {}",
                        module_info.name, module_info.version
                    );
                }
            }
            None => {
                if verbose {
                    println!("    installing {} version {}", module_info.name, module_info.version);
                }
            }
        }

        let lspk_installation_path = self.get_mod_file_path(format!("{}.pak", module_info.folder));

        if !dry_run {
            let mut file = OpenOptions::new()
                .write(true)
                .truncate(true)
                .create(true)
                .open(&lspk_installation_path)?;

            file.write_all(&decompressed_lspk.original_bytes)?;
        }

        existing_version_node.set_attribute_where_id(
            module_info.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(&module_info.uuid);

        if dry_run {
            print!("    non-dry run would have ");
        } else {
            self.write_settings_file(&parsed_settings)?;
        }

        println!("    installed {}\n", lspk_installation_path.display());

        Ok(())
    }
}

fn read_mod(mod_file_path: &str, mod_file: File) -> Result<DecompressedLspk> {
    let is_zipfile = Path::new(mod_file_path)
        .extension()
        .map_or(false, |e| e.eq_ignore_ascii_case("zip"));

    let named_file = if is_zipfile {
        lspk::entry_from_zipfile(&mod_file)?.1
    } else {
        lspk::Reader::new(mod_file)?.read()?
    };

    Ok(named_file)
}