loadsmith-loader 0.2.0

Mod loader definitions and launch-argument generation for the loadsmith mod-manager library
use std::{path::PathBuf, sync::LazyLock};

use globset::{GlobBuilder, GlobSet};
use loadsmith_install::{InstallRule, InstallRuleset, OwnedInstallRuleset, RouteRule};

use crate::{LaunchArgs, LaunchContext, Loader, Result, glob, glob_rule};

#[derive(Debug, Clone)]
pub struct Northstar {
    package_install_ruleset: OwnedInstallRuleset,
}

impl Northstar {
    pub fn with_rules(package_install_ruleset: OwnedInstallRuleset) -> Self {
        Self {
            package_install_ruleset,
        }
    }

    pub fn with_default_rules() -> Self {
        let exclude = GlobSet::builder()
            .add(glob!("manifest.json"))
            .add(glob!("README.md"))
            .add(glob!("icon.png"))
            .add(glob!("LICENSE"))
            .build()
            .expect("constant globs should be valid");

        let ruleset = OwnedInstallRuleset::from_rule_iter(
            vec![
                RouteRule::new_static("R2Northstar/mods")
                    .with_subdir(false)
                    .with_flatten(false),
            ],
            None,
        )
        .expect("rules are not empty so there should always be a valid default rule index")
        .with_exclude(exclude);

        Self::with_rules(ruleset)
    }

    pub fn add_install_rule(&mut self, rule: impl Into<InstallRule>) {
        self.package_install_ruleset.add_rule(rule.into());
    }

    pub fn insert_install_rule(&mut self, index: usize, rule: impl Into<InstallRule>) {
        self.package_install_ruleset.insert_rule(index, rule.into());
    }
}

impl Default for Northstar {
    fn default() -> Self {
        Self::with_default_rules()
    }
}

impl Loader for Northstar {
    fn id(&self) -> &'static str {
        "Northstar"
    }

    fn loader_install_rules(&self) -> InstallRuleset<'_> {
        static RULES: LazyLock<Vec<InstallRule>> = LazyLock::new(|| {
            vec![
                glob_rule!("Northstar/*" => ".")
                    .strip_top_level(true)
                    .use_links(true)
                    .into(),
            ]
        });

        InstallRuleset::new(&RULES)
    }

    fn package_install_rules(&self) -> InstallRuleset<'_> {
        self.package_install_ruleset.as_ref()
    }

    fn prepare_launch(&self, ctx: &LaunchContext) -> Result<()> {
        static GLOB_SET: LazyLock<GlobSet> = LazyLock::new(|| {
            GlobSet::builder()
                .add(
                    GlobBuilder::new("*.{dll,exe,bat}")
                        .literal_separator(true)
                        .build()
                        .expect("constant glob should be valid"),
                )
                .build()
                .expect("constant globs should be valid")
        });

        ctx.copy_glob_to_game(&GLOB_SET)
    }

    fn generate_launch_args(&self, ctx: &LaunchContext) -> Result<LaunchArgs> {
        let r2northstar_path = ctx.profile_path().join("R2Northstar");
        let path = ctx.format_proton_path(&r2northstar_path);

        let args = LaunchArgs::new()
            .arg("-northstar")
            .arg(format!("-profile={path}"));

        Ok(args)
    }

    fn package_config_dirs(&self) -> Vec<PathBuf> {
        Vec::new()
    }

    fn log_file(&self) -> Option<PathBuf> {
        None
    }

    fn proxy_dll(&self) -> Option<PathBuf> {
        None
    }
}

#[cfg(test)]
mod tests {
    use loadsmith_core::{PackageRef, Version};

    use crate::{assert_map, assert_maps, test_util::MapFileTester};

    use super::*;

    #[test]
    fn map_loader_files() {
        assert_maps!(MapFileTester::new(
            Northstar::with_default_rules(),
            PackageRef::new("northstar-Northstar".to_string(), Version::new(1, 31, 10)),
            true,
        ), [
            "README.md" => None,
            "Northstar/r2ds.bat" => "./r2ds.bat",
            "Northstar/R2Northstar/file.txt" => "./R2Northstar/file.txt",
            "Northstar/R2Northstar/plugins/DiscordRPC.dll" => "./R2Northstar/plugins/DiscordRPC.dll",
        ])
    }

    #[test]
    fn map_package_files() {
        assert_maps!(MapFileTester::new(
            Northstar::with_default_rules(),
            PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0)),
            false,
        ), [
            "manifest.json" => None,
            "README.md" => None,
            "icon.png" => None,
            "LICENSE" => None,
            "file.txt" => None,
            "mods/file.txt" => "R2Northstar/mods/file.txt",
            "mods/nested/file.txt" => "R2Northstar/mods/nested/file.txt",
            "nested/mods/file.txt" => "R2Northstar/mods/nested/file.txt",
        ]);
    }

    #[test]
    fn package_dir_works() {
        let loader = Northstar::with_default_rules();
        let package = PackageRef::new("Author-Name".to_string(), Version::new(1, 0, 0));

        assert!(loader.package_dir(&package).is_none());
    }
}