oliver 0.1.9

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
Documentation
use crate::Settings;
use anyhow::{anyhow, Result};
use larian_formats::bg3::{
    raw::{ModuleInfoNode, ModulesChildren},
    ModuleInfo,
};
use std::{collections::HashMap, path::PathBuf};

impl Settings {
    pub(crate) fn uninstall_all_helper(
        &self,
        dry_run: bool,
        mut ordered_indexes: Vec<usize>,
    ) -> Result<()> {
        let mut save = self.read_current_installed_mods()?;

        let Some(order_node) = save.find_node_by_id("ModOrder")?.children.first() else {
            return Ok(());
        };

        let mods_node = save.find_node_by_id("Mods")?;

        let unordered_entries: Result<HashMap<_, _>> = ordered_indexes
            .iter()
            .copied()
            .filter_map(|ordered_index| {
                get_unordered_index(order_node, ordered_index, mods_node)
                    .map(|possible_info| {
                        possible_info.map(|(info, index)| (info.uuid.clone(), (info, index)))
                    })
                    .transpose()
            })
            .collect();

        let unordered_entries = unordered_entries?;
        let mod_files_to_delete: Result<Vec<_>> = self
            .read_mod_files_dir()?
            .map(|e| {
                let entry = e?;

                if !entry.file_type()?.is_file() {
                    return Ok(None);
                }

                let file_name = PathBuf::from(entry.file_name());

                let extension = file_name.extension();
                if !extension.map_or(false, |e| e.eq_ignore_ascii_case("pak")) {
                    return Ok(None);
                }

                let pak = self
                    .read_mod_file(&file_name)?
                    .read()?
                    .extract_meta_lsx()?
                    .deserialize_as_mod_pak()?;

                Ok(unordered_entries
                    .get(pak.module_info.uuid.as_str())
                    .map(|(info, index)| (info, index, file_name)))
            })
            .collect();

        let mod_files_to_delete = mod_files_to_delete?;
        let mut indexes_to_delete = Vec::with_capacity(mod_files_to_delete.len());

        for mod_file_info in mod_files_to_delete {
            let Some((info, index, file_name)) = mod_file_info else {
                continue;
            };

            if dry_run {
                println!("{} would be uninstalled with a non-dry run", info.name);
            } else {
                println!("uninstalling {}", info.name);
                let mod_file = self.get_mod_file_path(file_name);

                std::fs::remove_file(mod_file)?;
            }

            indexes_to_delete.push(*index);
        }

        if !dry_run {
            indexes_to_delete.sort_unstable_by_key(|i| usize::MAX - i);

            let mods_node = save.find_node_mut_by_id("Mods")?;

            for i in indexes_to_delete {
                mods_node.children[0].node.remove(i);
            }

            let order_node = save.find_node_mut_by_id("ModOrder")?;

            ordered_indexes.sort_unstable_by_key(|i| usize::MAX - i);

            for i in ordered_indexes {
                order_node.children[0].node.remove(i);
            }

            self.write_settings_file(&save)?;
        }

        Ok(())
    }
}

fn get_unordered_index<'a>(
    ordered: &'a ModulesChildren,
    mod_index: usize,
    mods: &'a ModuleInfoNode,
) -> Result<Option<(ModuleInfo, usize)>> {
    let Some(order_entry) = ordered.node.get(mod_index) else {
        return Ok(None);
    };

    let uuid = order_entry
        .get_attribute_value_where_id(|id| id == "UUID")
        .ok_or_else(|| anyhow!("missing UUID for mod order index {mod_index}"))?
        .to_string();

    let Some(index) = mods.find_index_where_child_id(&uuid) else {
        return Ok(None);
    };

    let pak = mods.children[0].node[index].clone().try_into()?;

    Ok(Some((pak, index)))
}